1. 定长内存池配合脱离new申请空间
根据C++项目高并发内存池_定长内存池可知,使用定长内存池比直接new申请对象效率要高。
在PageCache向系统申请内存时我们使用new创建了许多Span对象来管理向系统申请的大块内存,这个Span对象是new出来的。以及在线程创建属于自己的ThreadCache时,也使用了new来申请ThreadCache对象,我们可以联系定长内存池,把这些申请过程用定长内存池替代,效率更高。
定长线程池:
#pragma once
#include"Common.h"
template<class T>//定长内存池
class ObjectPool {
private:
char* _memory;//指向大块内存的指针
//返回的内存用链式结构管理
void* _freeList;
size_t _overage;//大块内存剩余空间大小
public:
ObjectPool() :_memory(nullptr), _freeList(nullptr),_overage(0) {}
T* New() {
T* obj = nullptr;
if (_freeList != nullptr) {
//优先把归还的内存重复利用
//链表头删
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else
{
if (_overage < sizeof(T)) {
_overage = 100 * 1024;
_memory = (char*)SystemAlloc(_overage);
if (_memory == nullptr) {
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t SizeT = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
_memory += SizeT;
_overage -= SizeT;
}
//调用定位new进程空间初始化
new(obj)T;
return obj;
}
void Delete(T* obj) {
obj->~T();//显示调用析构函数,清理对象
//头插
*(void**)obj = _freeList;
_freeList = obj;
}
};
所以在PageCache中需要新定义成员定长内存池
#pragma once
#include"Common.h"
#include"ObjectPool.h"
class PageCache {
private:
SpanList _SpanList[NPAGE];
static PageCache _sInst;
//页号与Span链表的映射,方便归还内存时直接通过内存找页号找到这块内存是那个Span
std::unordered_map<PAGE_ID, Span*>IdSpanMap;
//定长内存池来申请Span对象
ObjectPool<Span>_SpanPool;
PageCache() {}
public:
std::mutex _PageMtx;
PageCache(const PageCache&) = delete;
//获取这个内存是那个Span
Span* MapObjToSpan(void* obj);
static PageCache* GetInst() {
return &_sInst;
}
//将CentralCache的Span回收,合并相邻页
void RetrunPageCache(Span* span);
//获取NumPage页的Span
Span* NewSpan(size_t NumPage);
};
在PageCache中每当申请/释放Span时用定长内存池New/Delete
线程在获得自己的Thread Cache时也使用定长内存池来申请对象
#pragma once
#include"Common.h"
#include"ThreadCache.h"
#include"PageCache.h"
#include"ObjectPool.h"
//线程调用申请ThreadCache空间
static void* ConcurrentAlloc(size_t size) {
if (size > MAX_BYTE) {//大于256KB内存
size_t AlignSize = SizeClass::RoundUp(size);//计算对其大小
//直接向PageCache索要K页的内存
size_t K = AlignSize >> PAGESIZE;
PageCache::GetInst()->_PageMtx.lock();
Span*span=PageCache::GetInst()->NewSpan(K);
PageCache::GetInst()->_PageMtx.unlock();
void* ptr = (void*)(span->_PageID << PAGESIZE);//获取这块内存的地址
return ptr;
}
else {
//获取线程自己的ThreadCache
if (tls_threadcache == nullptr) {
static ObjectPool<ThreadCache>TcPool;//使用定长内存池来获取对象
tls_threadcache = TcPool.New();
}
cout << std::this_thread::get_id() << " " << tls_threadcache << endl;
return tls_threadcache->ApplySpace(size);
}
}
static void ConcurrentFree(void* ptr, size_t size) {
if(size>MAX_BYTE){
Span* span = PageCache::GetInst()->MapObjToSpan(ptr);//计算要释放的大空间属于那个Span
PageCache::GetInst()->_PageMtx.lock();
PageCache::GetInst()->RetrunPageCache(span);//将内存挂到PageCache桶上,需要修改桶,所以要加锁
PageCache::GetInst()->_PageMtx.unlock();
}
else {
//释放时每个线程一定有tls_threadcache
assert(tls_threadcache != nullptr);
tls_threadcache->ReleaseSpace(ptr, size);
}
}
因为修改代码变化不大,为了避免冗余这里贴出链接
2. 释放空间时不需要传大小
在之前,我们的高并发内存池释放空间时需要传大小,十分的不方便。
传大小来释放空间,为了判断这块空间是从堆上申请的还是通过三层缓存来申请的。还可以判断这块内存在ThreadCache和CentralCache哈希桶中的那个桶。
因为PageCache中,已经实现了页号到Span*的映射。
所以知道要释放的地址,根据地址计算页号,在根据页号找到Span,每个Span中自由链表上挂的空间大小都是相同的,所以在Span中添加属性,记录切好的内存块的大小。
//管理以页为单位的大块空间结构
struct Span {
PAGE_ID _PageID;//记录是第几页
size_t _Num;//记录Span里面有多少页
Span* _next;//双向链表
Span* _prev;
size_t use_count;//记录分配了多少个对象给ThreadCahce
void* FreeList;//切好的小块内存空间
bool IsUse;//标记这块Span是否被使用
size_t ObjectSize;//每个切好的小块内存空间大小
Span() :_PageID(0), _Num(0), _next(nullptr), _prev(nullptr), use_count(0), FreeList(nullptr), IsUse(false), ObjectSize(0) {}
};
- 在Central Cache中将PageCache的大块内存切分成小内存时,就将Span中 ObjectSize字段记录下来。等到最后释放时根据页号找Span在找 ObjectSize字段就知道要释放内存的大小了。
- 如果申请大于256KB,没有经过CentralCache,直接向PageCache申请,此时需要在申请后手动填写 ObjectSize字段。
注意:
大于256KB时访问PageCache的哈希桶,而PageCache只能被一个线程访问,PageCache中的Span指针与页号的映射也是能由一个人访问,所以要对获取Span映射的函数添加锁。
Span* PageCache::MapObjToSpan(void* obj) {
std::unique_lock<std::mutex>lock(_PageMtx);//映射被多个线程访问需要加锁防止线程安全,出了函数锁自动释放
//计算obj的页号
PAGE_ID pageId = (PAGE_ID)obj >> PAGESIZE;
//获取这个内存是那个Span
std::unordered_map<PAGE_ID,Span*>::iterator ret = IdSpanMap.find(pageId);
if (ret != IdSpanMap.end()) {
return ret->second;
}
else {
assert(false);//不可能找不到
return nullptr;
}
}
因为修改代码变化不大,为了避免冗余这里贴出链接