我们的内存池已经完成了申请内存和释放内存的功能但是还有几个地方没有处理完善,主要有以下几个方面:

  • 大于256KB的内存申请
  • 内存池的内部要脱离new
  • 释放对象时的细节

大于256KB的内存处理

申请

在ConcurrentAlloc.h文件中,申请内存的时候要分两种情况

  • 小于256kb走thread cache
  • 大于256kb走page cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
// 申请的内存大于MAX_BYTES(256kb)
// 走Page Cache层申请

// 先进行对齐
size_t alignSize = SizeClass::RoundUp(size);

// 然后算需要几页
size_t kpage = alignSize >> PAGE_SHITF;
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(alignSize);
PageCache::GetInstance()->_pageMtx.unlock();

return (void*)(span->_pageId << PAGE_SHITF);
}
else
{
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
return pTLSThreadCache->Allocate(size);
}
}

我们SizeClass对齐的逻辑,并没有处理大于256KB的内存,我们需要增加这一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);

}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);

}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);

}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8*1024);

}
else
{
// 大于256KB 1<<13 也就是8kb
return _RoundUp(size, 1 << PAGE_SHITF);
}
}

但是这个大于256kb的让page cache申请,还是存在一些问题,因为page cache最多提供128页的内存,如果申请的内存大于128页,则还是需要进行处理,这部分,我们在page cache中进行处理。

PageCache::NewSpan函数中,增加申请大于128页的span的逻辑
1
2
3
4
5
6
7
8
9
10
11
12

// 如果k > 128页 走系统调用
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
Span* span = new Span;
span->_pageId = (PAGE_ID)ptr >> PAGE_SHITF;
span->_n = k;

_idSpanMap[span->_pageId] = span;
return span;
}

释放

大于256kb的内存,释放时,同样不能走thread cache层回收,要有单独的回收逻辑,我们还是通过pagecache层回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void ConcurrentFree(void* ptr, size_t size)
{

if (size > MAX_BYTES)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);

PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();

}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}

}

同样,在pagecache层中还要分为,这个内存是大于128页还是小于128页,小于128直接走Page cache层的回收逻辑,大于128页的通过系统调用回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 释放空闲span回到PageCache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHITF);
SystemFree(ptr);
delete span;
return;
}
// 合并相邻的span,向前合并,有问题:前面的span如果被用就不能进行合并
while (1)
{
PAGE_ID prevId = span->_pageId - 1;

auto ret = _idSpanMap.find(prevId);
//前边的页号没有了就不和并了
if (ret == _idSpanMap.end())
{
break;
}

// 前边相邻页在使用,不合并
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}

//超过128页的span也不合并
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}

// 进入合并的逻辑

//span->_pageId -= prevSpan->_n;
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;

_spanLists[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}


while (1)
{
PAGE_ID nextID = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextID);
if (ret == _idSpanMap.end())
{
break;
}

Span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}

//超过128页的span也不合并
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}

span->_n += nextSpan->_n;

_spanLists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}


_spanLists[span->_n].PushFront(span);
span->_isUse = false;

_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}

这里封装的了Windows系统的VirtualFree函数,进行大于128页的内存释放

1
2
3
4
5
6
7
8
9
// 调用系统接口释放内存
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// sbrk unmmap等
#endif
}

使用定长内存池配合脱离new

我们的内存池期望的是在内部不使用malloc函数申请内存,走自己的一套申请释放体系,但是目前我们的代码中,还是有不少地方(new span的对象)使用了new,为了完全脱离new,我们可以配合之前的定长内存池。

申请

因为span对象都是在page cache层new出来的,我们可以在page cache层增加一个定长内存池对象

1
ObjectPool<Span> _spanPool;

然后我们在new Span的场景下,替换成定长内存池的申请

1
2
3
4
5
Span* span = new Span;

//上边替换成下边

Span* span = _spanPool.New();

在线程申请创建thread cache时,也是通过new进行申请的,同样进行替换

1
2
3
4
5
6
if (pTLSThreadCache == nullptr)
{
/*pTLSThreadCache = new ThreadCache;*/
static ObjectPool<ThreadCache> tcPool;
pTLSThreadCache = tcPool.New();
}

释放

对应的delete也要替换成定长内存池的Delete

释放内存优化为不传对象大小

我们用free时,只需要传入一个指针即可,但是我们目前的内存池释放内存,不仅要传入指针,还需要传入待释放内存的大小,我们也需要优化为不传入对象的大小

我们可以在span对象中增加成员:ObjSize 用来记录span中每个对象的大小

1
size_t _ObjSize = 0; // 切好的小对象的大小

那我们的释放内存的函数就可以改为下边的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

static void ConcurrentFree(void* ptr)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_ObjSize;

if (size > MAX_BYTES)
{
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();

}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}

现在的问题就变成了,ObjSize的记录问题了,我们整个内存池中的span对象都是从Page cache层出来的,所以在调用NewSpan的时候,要将切分的对象的大小记录下来,我们申请小于256kb的对象时,都走的thread cache->central cache->page cache,另一种情况是大于256kb直接找page cache,这两个地方都需要记录ObjSize

1、在Central层调用NewSpan后,使得span->ObjSize=size

1
2
3
4
// 我们要计算向page cache层要多少页的span
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
span->_ObjSize = size;

2、CurrentAlloc中,申请大于256KB的对象

1
2
Span* span = PageCache::GetInstance()->NewSpan(alignSize);
span->_ObjSize = alignSize;

STL线程安全问题

我们通过unordered_map,建立了PAGE_ID和Span* 的联系,PageCache层,有读写操作,但是我们在使用PageCache的NewSpan函数时,都会加锁,但是CentralCache层和ConcurrentFree函数中会有读操作,而且并未加锁,这样,可能有线程正在改,就有线程读取,会有线程安全的问题

我们在MapObjectToSpan函数中加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHITF);
std::unique_lock<std::mutex> lock(_pageMtx);
if (_idSpanMap.find(id) != _idSpanMap.end())
{
return _idSpanMap[id];
}
else
{
assert(false);
return nullptr;
}
}