如果ThreadCache中的FreeList中空间节点超过要申请的长度,回收内存。
这些回收的内存节点给到Central Cache,Central Cache将这些空间节点合并成Span挂到Span哈希桶的对应位置。
#pragma once
#include"Common.h"
#include"CentralCache.h"
class ThreadCache {
private:
FreeList _FreeList[NUMLIST];
public:
//申请释放空间
void* ApplySpace(size_t size) {
assert(size <= MAX_BYTE);
size_t AligSize = SizeClass::RoundUp(size);
//计算桶位置
size_t index = SizeClass::Index(AligSize);
if (!_FreeList[index].Empty()) {
return _FreeList[index].Pop();
}
else {
//thread cache没有内存向central cache要空间
return RequestFromCentralCache(index, AligSize);
}
}
//释放的空间
void ReleaseSpace(void* ptr, size_t size) {
assert(size <= MAX_BYTE && ptr != nullptr);
size_t index = SizeClass::Index(size);//找到第几个桶
//将这个空间头插到自由链表上
_FreeList[index].Push(ptr);
//如果FreeList中空间节点超过要申请的长度,回收内存
if (_FreeList[index].GetMaxSize() < _FreeList[index].GetSize()) {
RetrunCentralCache(_FreeList[index], size);//size是每个节点的大小
}
}
//释放空间到CentralCache
void RetrunCentralCache(FreeList& List, size_t size) {
void* start = nullptr; void* end = nullptr;
List.PopList(start, end, List.GetMaxSize());//一次获取List.GetMaxSize()个节点的链表
CentralCache::GetCentralCache()->MergeSpan(start, size);
}
//向中心缓存申请空间
void* RequestFromCentralCache(size_t index, size_t size) {
//慢开始调节算法
size_t Num = min(SizeClass::ForMemory(size),_FreeList[index].GetMaxSize());//计算中心层给线程缓存多少个空间节点;
if (Num == _FreeList[index].GetMaxSize()) {
_FreeList[index].GetMaxSize() += 2;//慢增长,每次申请下次会多给ThreadCache空间
}
//依次递增SizeClass::ForMemory(size)是申请上限,一次申请数量不会比它还大
void* begin = nullptr; void* end = nullptr;
size_t actualNum = CentralCache::GetCentralCache()->GiveThreadCache(begin, end, Num, size);
assert(actualNum >= 1);//至少会申请到一个空间节点。
if (actualNum == 1) {//实际就获得了一个节点,直接将这个节点返回
return begin;
}
else {
//获得了多个节点,要把这些节点都插入到ThreadCache的哈希桶上
_FreeList[index].PushList(NextObj(begin), end, actualNum - 1);//将链表的下一个节点插入桶中,头节点返回给ThreadCache
return begin;
}
}
};
//每个线程都会拥有自己的ThreadCache
static __declspec(thread) ThreadCache* tls_threadcache = nullptr;//用static修饰只在当前文件可见
自由链表类需要添加的成员函数
//管理切分好内存的自由链表
class FreeList {
public:
FreeList() :_freeList(nullptr), MaxSize(1), size(0) {}
void Push(void* obj) {
//头插
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
++size;
}
void* Pop() {
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
--size;
return obj;
}
void PushList(void* begin, void* end, size_t len) {
//头插链表
NextObj(end) = _freeList;
_freeList = begin;
size += len;
}
void PopList(void*& begin, void*& end, size_t len) {//获得len个内存节点
assert(len >= size);//要弹出的节点个数大于现在freeList中的节点个数
begin = _freeList; end = begin;
for (size_t i = 0; i < len - 1; i++) {
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
size -= len;
}
bool Empty() { return _freeList == nullptr; }
size_t GetSize() { return size; }
size_t& GetMaxSize() { return MaxSize; }
private:
void* _freeList;
size_t MaxSize;//记录freeList链表要向中心缓存层申请多少个内存节点
size_t size;//记录FreeList有几个节点
};
需要注意的是,归还内存的时候需要修改CentralCache的Span链表,需要加桶锁。
此外,线程归还内存时这块内存属于Span链表中的那个Span不确定。
如果直到一块空间的页号,这块空间的地址为页号×页大小(这块内存任意位置÷页大小都是页号)用来判断这块内存属于那一页
eg:
void TestPageId() {
PAGE_ID id = 2020;
PAGE_ID id2 = 2021;//1页大小
char* start = (char*)(id << PAGESIZE);
char* end = (char*)(id2 << PAGESIZE);
cout << (void*)start << " " << (void*)end << endl;
while (start <= end) {
cout << (void*)start << " PAGEID:" << ((PAGE_ID)start >> PAGESIZE) << endl;
start += 8;//一块空间8字节
}
}
int main()
{
TestPageId();
return 0;
}
综上,知道一块内存的地址,可以确定这块内存的页号。为了方便找到那个Span,这里选择添加页号与Span链表的映射std::unordered_map
这个映射放到PageCache上
CentralCache
#pragma once
#include"Common.h"
//中心缓存只有一个,每个线程都可以看见,所以中心缓存类采用单例模式(饿汉模式)
class CentralCache {
private:
SpanList _SpanList[NUMLIST];//与ThreadCache桶的大小相同
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
public:
CentralCache() {}
static CentralCache* GetCentralCache() {
return &_sInst;
}
//获取一个非空Span,需要向PageCache申请空间
Span* GetSpan(SpanList& List, size_t size);
size_t GiveThreadCache(void*& start, void*& end, size_t Num, size_t Size);
//将一条空间链表合并成Span挂到桶上,实现释放内存的过程
void MergeSpan(void* start, size_t size);
};
#include"CentralCache.h"
#include"PageCache.h"
CentralCache CentralCache::_sInst;//定义了一个对象
size_t CentralCache::GiveThreadCache(void*& start, void*& end, size_t Num, size_t size) {
size_t index = SizeClass::Index(size);
//要求num个内存节点,需要计Span中FreeList空节点个数
_SpanList[index]._mtx.lock();//加锁
Span* span = GetSpan(_SpanList[index], size);
assert(span != nullptr && span->FreeList != nullptr);
start = span->FreeList; end = start;
//变量Num次,找Num个节点
size_t actualNum = 1;//实际获得几个内存节点
for (int i = 0; i < Num - 1; i++) {
if (NextObj(end) == nullptr) break;//如果走到span中FreeList的空,说明span中内存不够Num个,这个时候有多少返回多少
actualNum += 1;
end = NextObj(end);
}
span->FreeList = NextObj(end);
//为分出去的内存节点链表添加nullptr
NextObj(end) = nullptr;
span->use_count += actualNum;
_SpanList[index]._mtx.unlock();
return actualNum;
}
Span* CentralCache::GetSpan(SpanList& List, size_t size) {
//在哈希桶对应位置Span链表中找是否有Span,没有就向PageCache申请空间
//遍历桶的Span链表
Span* it = List.begin();
while (it != List.end()) {
if (it->FreeList != nullptr) {
return it;//这个Span有空间
}
else {
//Span没有空间,继续找下一个链表Span
it = it->_next;
}
}
//先把CentralCache的桶锁解开,如果其他线程释放内存不会阻塞
List._mtx.unlock();
//没有空闲的Span只能找PageCache,需要加锁,PageCache只能由一个线程访问
//size是单个对象的大小
PageCache::GetInst()->_PageMtx.lock();
Span* span=PageCache::GetInst()->NewSpan(SizeClass::NumForPage(size));
span->IsUse = true;
PageCache::GetInst()->_PageMtx.unlock();
//获得了一块大Span,这块Span这时被线程单独看到,不需要加锁(没有挂到桶上)
//Span起始地址
char* start = (char*)((span->_PageID) << PAGESIZE);
size_t ByteSize = (span->_Num) << PAGESIZE;
char* end = start + ByteSize;
//把Span内部大块内存切成自由链表链接起来
span->FreeList = start;
start += size;//自由链表的头节点
void* tail = span->FreeList;
while (start < end) {
NextObj(tail) = start;
tail = NextObj(tail);
start += size;
}
List._mtx.lock();
List.Insert(List.begin(), span);//将Span挂到桶上,此时需要加桶锁
return span;
}
void CentralCache::MergeSpan(void* start, size_t size) {
size_t index = SizeClass::Index(size);
_SpanList[index]._mtx.lock();
while (start != nullptr) {
void* next = NextObj(start);
Span* span = PageCache::GetInst()->MapObjToSpan(start);
//将这个节点头插到这个Span上
NextObj(start) = span->FreeList;
span->FreeList = start;
span->use_count--;//每回收一块内存,记录分出去的内存use_count就-1
if (span->use_count == 0) {
//Span所有小块内存都回来了,这个Span就可以给Page Cache回收。PageCache做前后页的合并
_SpanList[index].Erase(span);
//通过页号就可以再次找到这块内存
span->FreeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
//页号与页数不能动,PageCache通过这两个数据找到这大块内存,此时可以解开桶锁,让其他线程访问这个桶
_SpanList[index]._mtx.unlock();
//此时要访问PageCache需要加PageCache的锁
PageCache::GetInst()->_PageMtx.lock();
PageCache::GetInst()->RetrunPageCache(span);
PageCache::GetInst()->_PageMtx.unlock();
_SpanList[index]._mtx.lock();
}
start = next;
}
_SpanList[index]._mtx.unlock();
}
PageCache添加映射
#pragma once
#include"Common.h"
class PageCache {
private:
SpanList _SpanList[NPAGE];
static PageCache _sInst;
//页号与Span链表的映射,方便归还内存时直接通过内存找页号找到这块内存是那个Span
std::unordered_map<PAGE_ID, Span*>IdSpanMap;
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);
};
#include"PageCache.h"
PageCache PageCache::_sInst;
Span* PageCache::NewSpan(size_t NumPage) {//NumPage是页数
assert(NumPage > 0 && NumPage < NPAGE);
//看当前位置桶中是否有Span
if (!_SpanList[NumPage].Empty()) {
return _SpanList->Pop();
}
//对应位置的桶是空,检查后面桶里有没有Span,将大Span切分成小Span
for (size_t i = NumPage + 1; i < NPAGE; i++) {
if (!_SpanList[i].Empty()) {//有一个桶存在,切分大Span成NumPage页Span和N-NumPage页的Span
Span* NumPageSpan = new Span;
Span* NSpan = _SpanList[i].Pop();
//头切
NumPageSpan->_PageID = NSpan->_PageID;
NumPageSpan->_Num = NumPage;
NSpan->_PageID += NumPage;
NSpan->_Num -= NumPage;
//将切下的Span挂到其他桶上
_SpanList[NSpan->_Num].Insert(_SpanList[NSpan->_Num].begin(), NSpan);
//填入PAGE_ID与Span*映射中
for (PAGE_ID i = 0; i < NumPageSpan->_Num; i++) {//切了Num页
IdSpanMap[NumPageSpan->_PageID + i] = NumPageSpan;
}
return NumPageSpan;
}
}
//所有桶都没有Span,直接向堆中申请一大块空间,将这块空间挂到最后一个桶上
Span* BigSpan = new Span;
void* ptr = SystemAlloc(NPAGE - 1);
BigSpan->_PageID = (PAGE_ID)ptr >> PAGESIZE;
BigSpan->_Num = NPAGE - 1;
_SpanList[BigSpan->_Num].Insert(_SpanList[BigSpan->_Num].begin(), BigSpan);
//在调用自己向Central Cache发送空间
return NewSpan(NumPage);
}
Span* PageCache::MapObjToSpan(void* obj) {
//计算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;
}
}
//将CentralCache的Span回收,合并相邻页
void PageCache::RetrunPageCache(Span* span) {
}
获得CentralCache的Span后,对Span前后页进行合并,缓解内存碎片外碎片问题。
为了找前后页对应的Span还需要页号和Span的映射,这个映射在上一层已经介绍。
如果前后页在PageCache桶上就可以合并,如果还在被CentralCache使用则不能被合并。
注意:这时不能看Span中use_cout因为在对线程的情况下,可能线程1还没有改变use_cout时,线程2来将这块空间又合并到PageCache。存在线程安全问题
这时添加一个状态IsUse标记这个Span是否被使用
//管理以页为单位的大块空间结构
struct Span {
PAGE_ID _PageID;//记录是第几页
size_t _Num;//记录Span里面有多少页
Span* _next;//双向链表
Span* _prev;
size_t use_count;//记录分配了多少个对象给ThreadCahce
void* FreeList;//切好的小块内存空间
bool IsUse;//标记这块Span是否被使用
Span() :_PageID(0), _Num(0), _next(nullptr), _prev(nullptr), use_count(0), FreeList(nullptr), IsUse(false) {}
};
PageCache
#pragma once
#include"Common.h"
class PageCache {
private:
SpanList _SpanList[NPAGE];
static PageCache _sInst;
//页号与Span链表的映射,方便归还内存时直接通过内存找页号找到这块内存是那个Span
std::unordered_map<PAGE_ID, Span*>IdSpanMap;
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);
};
#include"PageCache.h"
PageCache PageCache::_sInst;
Span* PageCache::NewSpan(size_t NumPage) {//NumPage是页数
assert(NumPage > 0 && NumPage < NPAGE);
//看当前位置桶中是否有Span
if (!_SpanList[NumPage].Empty()) {
return _SpanList[NumPage].Pop();
}
//对应位置的桶是空,检查后面桶里有没有Span,将大Span切分成小Span
for (size_t i = NumPage + 1; i < NPAGE; i++) {
if (!_SpanList[i].Empty()) {//有一个桶存在,切分大Span成NumPage页Span和N-NumPage页的Span
Span* NumPageSpan = new Span;
Span* NSpan = _SpanList[i].Pop();
//头切
NumPageSpan->_PageID = NSpan->_PageID;
NumPageSpan->_Num = NumPage;
NSpan->_PageID += NumPage;
NSpan->_Num -= NumPage;
//将切下的Span挂到其他桶上
_SpanList[NSpan->_Num].Insert(_SpanList[NSpan->_Num].begin(), NSpan);
//保存NSpan前后页的映射关系,方便回收
IdSpanMap[NSpan->_PageID] = NSpan;
IdSpanMap[NSpan->_PageID + NSpan->_Num - 1] = NSpan;//中间页没必要加入映射中,前后两页为了方便回收
//填入PAGE_ID与Span*映射中
for (PAGE_ID i = 0; i < NumPageSpan->_Num; i++) {//切了Num页
IdSpanMap[NumPageSpan->_PageID + i] = NumPageSpan;
}
return NumPageSpan;
}
}
//所有桶都没有Span,直接向堆中申请一大块空间,将这块空间挂到最后一个桶上
Span* BigSpan = new Span;
void* ptr = SystemAlloc(NPAGE - 1);
BigSpan->_PageID = (PAGE_ID)ptr >> PAGESIZE;
BigSpan->_Num = NPAGE - 1;
_SpanList[BigSpan->_Num].Insert(_SpanList[BigSpan->_Num].begin(), BigSpan);
//在调用自己向Central Cache发送空间
return NewSpan(NumPage);
}
Span* PageCache::MapObjToSpan(void* obj) {
//计算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;
}
}
//将CentralCache的Span回收,合并相邻页
void PageCache::RetrunPageCache(Span* span) {
//对Span前后页进行合并,缓解内存碎片外碎片问题
while (true) { //向前合并
PAGE_ID prevId = span->_PageID - 1;
std::unordered_map<PAGE_ID, Span*>::iterator ret = IdSpanMap.find(prevId);
if (ret == IdSpanMap.end()) {
break;
}
else {
if (ret->second->IsUse == true) {
//前面相邻页的Span正在使用的内存不合并
break;
}
//和前Span合并大小超过128KB则停止合并,无法管理
else if (ret->second->_Num + span->_Num >= NPAGE) {
break;
}
else {
//合并
span->_PageID = ret->second->_PageID;
span->_Num += ret->second->_Num;
_SpanList[ret->second->_Num].Erase(ret->second);
delete ret->second;
}
}
}
//向后合并
while (true) {
PAGE_ID nextId = span->_PageID + span->_Num;
std::unordered_map<PAGE_ID, Span*>::iterator ret = IdSpanMap.find(nextId);
if (ret == IdSpanMap.end()) {
break;
}
else {
Span* nextSpan = ret->second;
if (nextSpan->IsUse == true) {
break;
}
else if (nextSpan->_Num + span->_Num >= NPAGE) {
break;
}
else {
span->_Num += nextSpan->_Num;
_SpanList[nextSpan->_Num].Erase(nextSpan);
IdSpanMap.erase(nextSpan->_Num);
delete nextSpan;
}
}
}
//将合并的Span挂起并添加映射
_SpanList[span->_Num].Insert(_SpanList[span->_Num].begin(), span);
span->IsUse = false;
IdSpanMap[span->_PageID] = span;
IdSpanMap[span->_PageID + span->_Num - 1] = span;
}
#include"ConcurrentAlloc.h"
void f1() {
std::vector<void*>vet;
for (int i = 0; i < 10; i++) {
void* ptr = ConcurrentAlloc(6);
vet.push_back(ptr);
}
for (auto e : vet) {
ConcurrentFree(e, 6);
}
}
void f2() {
std::vector<void*>vet;
for (int i = 0; i < 10; i++) {
void* ptr = ConcurrentAlloc(7);
vet.push_back(ptr);
}
for (auto e : vet) {
ConcurrentFree(e, 7);
}
}
void TestConcurrentAllocThread() {
std::thread t1(f1);
t1.join();
std::thread t2(f2);
t2.join();
}
int main()
{
TestConcurrentAllocThread();
return 0;
}
运行结果:
走到这里项目主体基本完成剩下的就是效率测试与优化
Github
Gitee