实际问题
在多个线程频繁进行小块内存的申请和释放的情况,多个进程向内核申请内存,带来的进程之间激烈竞争,大大减少了工作效率,同时小块内存带来的内存碎片问题会降低内存的使用效率。
设计思路
1.每个线程有自己独立的一块内存,小的内存块到这上面去切。
2.线程需求的内存块大小不同,为其划分不同的大小内存块,这些内存块是切好的自由链表,并且按照哈希桶的方式存储,这样线程thread_cache就能轻松的为线程获取对应大小的内存块。每个线程都有其私有的thread_cache
3.在thread_cache之上有公有的central_cache,central_cache同样是哈希桶结构,每个桶里有一块或几块固定大小的内存块,这些内存块有在其内被切成自由链表,thread没有内存向central申请,thread闲置内存过多,向central退还。 central在进程内只有唯一一份。
4.pagecache,最底层,同样是哈希桶结构,负责向系统申请内存,并将申请到的指针转化为页号(64和32转化不同),同时负责向central发放内存,并回收小块的页将其合并为大块内存。
基础结构见下图(手绘拙劣)
设计主体
1.定长内存池+线程池
用于摆脱系统申请malloc和缓解批量线程问题。
定长内存池,线程池实现方法很多就不赘述了。
class ObjectPool {
public:
//内存申请
T* New()
{
//返回的指针
T* obj = nullptr;
//判断自由链表是否为空
if (_freeList)
{
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else {
//申请空间,并且一小块一小块的给
if (_remain < sizeof(T))
{
_remain = 1024 * 128;
/* _memory = (char*)malloc(1024 * 128);通过malloc内存池申请空间*/
_memory = (char*)SystemAlloc(_remain >> 13);//直接向堆申请空间
if (_memory == nullptr)
{
printf("New error\n");
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remain -= objSize;
}
new(obj)T;
return obj;
}
void Delete(T*obj)
{
//内存返回
obj->~T();
//内存放进自由队列,模板本身自己析构。
*(void**)obj = _freeList;
_freeList = obj;
}
private:
//大块内存,开始时的头指针
char* _memory = nullptr;
//剩余多少内存可用
size_t _remain = 0;
//自由链表,自由链表的起始者
void* _freeList = nullptr;
};
2.thread cache
class ThreadCache
{
public:
void* Allocate(size_t size);
void Delallocate(void*ptr,size_t size);
//从CentralCache申请内存
void* FetchFromCentralCache(size_t index, size_t size);
void ListTooLong(FreeList& List, size_t size);
private:
FreeList _freeLists[NFREELIST];
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
class FreeList
{
private:
size_t _listLength = 0;
size_t _maxSize = 1;
void* _freeList = nullptr;
};
From是从central获取span中切分好的自由链表,然后内存被使用完后返回到_freeLists[NFREELIST]中的哈希桶。
TooLong 是返回的自由链表过长后将内存返回给central。
这里的内存申请采用了慢增长策略,并非一次给最大值的数量的小内存块,并且此慢增长机制同样用于什么时候还内存给central。
//先确定申请多少,慢启动策略。
size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumFromCentral(size));
//每次多申请一点点。
if (batchNum == _freeLists[index].MaxSize())
{
_freeLists[index].MaxSize() += 1;
}
随着MaxSize慢慢变大一次能申请的小块内存才增多。
在返回时也是挂在thread_cache中哈希桶的自由链表长度超过MaxSize就还内存给central
3.central_cache
这是承上启下的一块内容,这里要全局只有一个对象,故采用单例模式进行设计。
class CentralCache
{
public:
//获得单例模式对象
static CentralCache& GetCenObject()
{
return _sInst;
}
//向pagecache申请span
Span* ApplyNewSpan(SpanList& list,size_t size);
//向threadcache对应桶的自由链表注意加锁
size_t ApplyCentralList(size_t size, size_t batchNum, void*& start, void*& end);
//过长的_freelist归还回来
void GiveBackToCentral(void* start, size_t size);
private:
SpanList _spanLists[NFREELIST];
//单例模式
CentralCache() {}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
};
struct Span
{
int _pageId = 0;
int _pageSpan = 0;
void* _freelist = nullptr ;
int _useCount = 0;
bool _isUse = false;
int _sizeObj = 0;
Span* _prev = nullptr;
Span* _cur = nullptr;
};
class SpanList
{
public:
std::mutex _mtx;
//增删......
private:
Span* _head;
};
所有线程都要访问central故哈希桶中需要加桶锁,防止线程冲突。当对应的哈希桶中没有span时向page_cache申请,申请回来之后要对其进行自由链表的划分处理,当span中的usecount归零时证明这一整块内存都没使用要返回给pagecache。
4.page_cache
class PageCache
{
public:
//获得单例模式对象
static PageCache& GetPageObject()
{
return _pInst;
}
//Cen向page申请span
Span* ApplyPageSpan(size_t size);
//获得页和span的映射关系
Span* ApplyIdToSpan(void*ptr );
void GiveBackToPage(Span* span);
std::mutex _pageMtx;
private:
ObjectPool<Span> _spanPool;
/*std::unordered_map<PAGE_ID, Span*> _idToSpan;用基数树性能优化*/
TCMalloc_PageMap1<32 - PAGE_SHIFT> _idToSpan;
SpanList _spanLists[NPAGES];
static PageCache _pInst;
PageCache() {}
PageCache(const PageCache&) = delete;
};
这里也使用了单例模式。申请的内存指针地址采用了基数树进行映射,建立起对应关系。
当central申请内存时,从page对应(映射)桶取span,没有就向page下一个桶取,取到了大的span将其进行切分,一份给申请的central桶,一份挂到page小桶中。直到最后的桶中也没有128页的span,就向系统申请128页的内存(8k一页),进行切分。
当central中span还到page时,会检查有没有和个span相邻页数的span,有就进行合并,直到没有相邻页或者长度会超过128页就停止。
这里在申请大块内存时,使用了复用机制,避免了再次写切分代码的冗余。
//走到这里就要向底层申请128页的大页内存,并回调一次
Span* maxSpan = _spanPool.New();;
maxSpan->_pageSpan = NPAGES - 1;
//申请内存
void* ptr = SystemAlloc(NPAGES - 1);
maxSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
_spanLists[NPAGES - 1].PushFront(maxSpan);
return ApplyPageSpan(k);
项目效果
项目总结
实际上这个项目还有很多细节需要控制,我在调试时遇到了很多次野指针问题,非常难调试,而且不一定每次调试都会出现错误,其中有一次就是合并之后没有挂回page。
具体代码见此:https://gitee.com/bfzxie/high-concurrency-memory-pool/tree/master/ConcurrentMemoryPool