在高性能内存池的设计中,内存释放的逻辑直接决定了碎片率、并发效率和整体性能。本文基于自研的三级缓存内存池(ThreadCache → CentralCache → PageCache),拆解从用户调用ConcurrentFree开始的完整释放链路,聚焦 "正确释放 + 高效复用" 的核心逻辑
1.释放入口:用户层的 ConcurrentFree
内存释放的入口是ConcurrentFree函数,这是暴露给用户的唯一释放接口,核心作用是将释放请求转发到当前线程私有的ThreadCache:
// ConcurrentAlloc.h
static void ConcurrentFree(void* ptr, size_t size) //用户层释放接口
{
assert(pTLSThreadCache);
assert(ptr);
assert(size <= MAX_BYTES);
pTLSThreadCache->Deallocate(ptr, size); //转发到ThreadCache层
}
这里的关键是pTLSThreadCache是线程局部存储(TLS)的指针,每个线程有独立的ThreadCache,释放操作先在 "线程私有空间" 完成,避免跨线程竞争
2.第一步:ThreadCache 层的释放(私有缓存,无锁)
ThreadCache::Deallocate是释放流程的第一层,核心逻辑是 "先缓存,超阈值再批量归还"
// ThreadCache.cpp
void ThreadCache::Deallocate(void* ptr, size_t size) //释放到ThreadCache
{
assert(ptr);
assert(size <= MAX_BYTES);
size_t index = SizeClass::Index(size); //计算内存块对应FreeList桶下标
_freeList[index].push(ptr); //头插法将内存块归还给对应FreeList
// 检查FreeList长度:超过慢启动阈值则批量释放到CentralCache
if (_freeList[index].Size() >= _freeList[index].GetMaxSize())
{
ListTooLong(_freeList[index], size); //触发批量释放
}
}
- 核心规则:
ThreadCache为每个规格的内存块维护独立的FreeList,释放的内存块先缓存到链表中 - 慢启动机制:每个
FreeList有动态阈值_maxSize,只有当缓存的内存块数量超过阈值时,才会调用ListTooLong批量释放到CentralCache,避免频繁的跨层交互
ListTooLong的逻辑是批量取出_maxSize个内存块,转发到CentralCache
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.rangePop(start, end, list.GetMaxSize()); //批量弹出内存块
CentralCache::GetInstance()->ReleaseListToSpans(start, size); //转发到CentralCache
}
3.第二步:CentralCache 层的释放(全局缓存,桶锁保护)
CentralCache::ReleaseListToSpans是释放流程的核心中转层,负责将ThreadCache归还的内存块归还给对应的span,并在span完全空闲时归还到PageCache。核心逻辑拆解如下:
1. 加桶锁:保证并发安全
// CentralCache.cpp
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanList[index]._mutex.lock(); //加对应桶的细粒度锁,避免全局竞争
while (start)
{
// 步骤1:取下一个待释放的内存块(避免遍历中链表断裂)
void* next = NextObj(start);
// 步骤2:通过内存块地址找到对应的span(核心:_idSpanMap映射)
span* span = PageCache::GetInstance()->MapObjectToSpan(start);
// 步骤3:将内存块头插回span的_freeList(归还小块内存)
NextObj(start) = span->_freeList;
span->_freeList = start;
// 步骤4:span的使用计数-1(记录已归还的小块数量)
span->_useCount--;
// 步骤5:若span的所有小块都已归还,将span归还到PageCache
if (span->_useCount == 0)
{
// 从CentralCache的桶中移除该span
_spanList[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
// 解锁桶锁→加PageCache全局锁→释放span→重新加桶锁(避免死锁)
_spanList[index]._mutex.unlock();
PageCache::GetInstance()->_pageMutex.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMutex.unlock();
_spanList[index]._mutex.lock();
}
start = next; //遍历下一个内存块
}
_spanList[index]._mutex.unlock(); //解锁桶锁
}
- 桶锁的价值:
CentralCache为每个规格维护独立的spanList桶,每个桶有专属_mutex,释放操作仅锁定当前桶,而非全局,最大化并发效率 - 关键判断
_useCount == 0:只有当span切分的所有小块内存都已归还时,才会将span归还给PageCache,保证span的 "完整复用"
4.第三步:PageCache 层的释放(页级缓存,合并碎片)
PageCache::ReleaseSpanToPageCache是释放流程的最后一层,核心目标是将空闲的span合并成更大的span,减少内存碎片,其核心逻辑是 "双向合并 + 首尾页映射更新"
// PageCache.cpp
void PageCache::ReleaseSpanToPageCache(span* obj)
{
// 1. 前向合并:查找当前span的前一页,判断是否可合并
while (1)
{
PAGE_ID prevId = obj->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
if (ret == _idSpanMap.end()) break; //前一页无映射,无法合并
span* prevSpan = ret->second;
if (prevSpan->_isUse == true) break; //前span正在使用,无法合并
if (prevSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1) break; //合并后超最大页限制
// 合并前span到当前span
obj->_pageId = prevSpan->_pageId;
obj->_n += prevSpan->_n;
_spanlist[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
// 2. 后向合并:查找当前span的后一页,判断是否可合并(逻辑同前向)
while (1)
{
PAGE_ID nextId = obj->_pageId + obj->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end()) break;
span* nextSpan = ret->second;
if (nextSpan->_isUse == true) break;
if (nextSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1) break;
// 合并后span到当前span
obj->_n += nextSpan->_n;
_spanlist[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
// 3. 将合并后的span加入PageCache的对应桶,并更新首尾页映射
_spanlist[obj->_n].Push_front(obj);
obj->_isUse = false; //标记为空闲
_idSpanMap[obj->_pageId] = obj; //更新首页映射
_idSpanMap[obj->_pageId + obj->_n - 1] = obj; //更新尾页映射
}
- 双向合并:先合并前序空闲 span,再合并后序空闲 span,最大化减少碎片
- 首尾页映射优化:仅更新合并后 span 的首尾页到
_idSpanMap(而非全量页),因为空闲 span 不会被MapObjectToSpan访问(只有分配态 span 需要全量映射),大幅降低哈希表维护成本
5.释放流程总结
整个内存释放流程遵循 "分层缓存、按需合并、细粒度锁" 的核心设计:
- 优先释放到线程私有
ThreadCache,无锁且高效 - 超过阈值后批量释放到
CentralCache,桶锁保证并发安全 span完全空闲时归还到PageCache,双向合并减少碎片- 首尾页映射优化
_idSpanMap,兼顾正确性与性能
如果代码进行到这里,说明我们写的高并发内存池已经完成了80%,接下来我们进行优化代码,就算完成这个项目啦~~
老样子,下面是总代码~~
6.全部代码
1.CentralCache.h
cpp
#pragma once
#include "Common.h"
//单例模式(饿汉模式)
class CentralCache
{
public:
//获取实例
static CentralCache* GetInstance()
{
return &_sInst;
}
// 获取⼀个⾮空的span
span* GetOneSpan(spanList& list, size_t size);
// 从中⼼缓存获取⼀定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
//将一点数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
private:
spanList _spanList[MAX_BUCKETS]; //和ThreadCheche逻辑是一样的,对应字节找对应桶
private:
CentralCache()
{ }
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
};
2.Common.h
cpp
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <time.h>
#include <assert.h>
#include <thread>
#include <mutex>
#ifdef _WIN32
#include <windows.h>
#else
//linux...
#endif
static const size_t MAX_BYTES = 262144; //1024 * 256 -> 256kb //最大字节数
static const size_t MAX_BUCKETS = 208; //最大_freeList桶数量
static const size_t MAX_PAGE_BUCKETS = 129; //最大_pageList桶数量(128,但是是直接映射,没有0页这种说法,所以我们直接将128写为129,不要下标0)
static const size_t PAGE_SHIFT = 13; //定义一页8k,也就是1024 * 8 = 2^13字节
//如果在32位下,假设每一个页大小是8k,也就是2^13,那么最多有2^32 / 2^13 = 2^19,只有524,288个
//size_t完全够用
//但是在64位下,每一个页大小是8k,那么就有2^64 / 2^13 = 5,070,602,400,912,917,605,986,812,821,504个
//size_t不够用,只能使用unsigned long long
#ifdef _WIN64
using PAGE_ID = unsigned long long;
#elif _WIN32
using PAGE_ID = size_t;
#endif
// 直接去堆上按⻚申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage * (1 << PAGE_SHIFT), MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
class FreeList //定义一个FreeList类,因为不管是哪个自由链表桶都是一样的结构
{
public:
FreeList()
: _freeList(nullptr)
, _size(0)
, _maxSize(1)
{}
void push(void* obj) //头插
{
assert(obj != nullptr);
NextObj(obj) = _freeList;
_freeList = obj;
_size++;
}
void rangePush(void* start, void* end, size_t range) //范围头插
{
NextObj(end) = _freeList;
_freeList = start;
_size += range;
}
void* pop() //头删
{
assert(_freeList != nullptr);
void* obj = _freeList;
_freeList = NextObj(_freeList);
_size--;
return obj;
}
void rangePop(void*& start, void*& end, size_t range) //范围头删
{
assert(range <= _size);
start = _freeList;
end = start;
for (size_t i = 0; i < range - 1; i++)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= range;
}
bool empty()
{
return _freeList == nullptr;
}
size_t& GetMaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList;
size_t _size;
size_t _maxSize;
};
class SizeClass //确定哪个桶
{
public:
// 整体控制在最多10%左右的内碎⽚浪费
// 字节数 对齐数 桶的数量
// [1,128] 8byte对⻬ freelist[0,16) -> 16个桶
// [128+1,1024] 16byte对⻬ freelist[16,72) -> 56个桶
// [1024+1,8*1024] 128byte对⻬ freelist[72,128) -> 56个桶
// [8*1024+1,64*1024] 1024byte对⻬ freelist[128,184) -> 56个桶
// [64*1024+1,256*1024] 8*1024byte对⻬ freelist[184,208) -> 24个桶
// 这样可以让桶的数量只有208个,比起每8个字节为一个桶节省了很多
//普通写法
/*
static inline size_t _RoundUp(size_t bytes, size_t align) //对齐,比如7字节对齐8字节,9字节对齐16字节,129字节对齐144字节
{
if (bytes % align == 0)
{
return bytes; //无需对齐,因为已经对齐了
}
//2 -> 8 (2 / 8 + 1) * 8 -> 8
//129 -> 144 (129 / 16 + 1) * 16 -> 144
return (bytes / align + 1) * align;
}
*/
//位运算写法
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return (((bytes) + alignNum - 1) & ~(alignNum - 1));
}
static inline size_t RoundUp(size_t bytes) //对齐
{
assert(bytes <= MAX_BYTES);
if (bytes <= 128)
{
return _RoundUp(bytes, 8);
}
else if (bytes <= 1024)
{
return _RoundUp(bytes, 16);
}
else if (bytes <= 8192)
{
return _RoundUp(bytes, 128);
}
else if (bytes <= 65536)
{
return _RoundUp(bytes, 1024);
}
else if (bytes <= 262144)
{
return _RoundUp(bytes, 8192);
}
else
{
assert(false);
return -1;
}
}
//普通写法
/*
static inline size_t _Index(size_t bytes, size_t alignNum) //计算映射的哪⼀个⾃由链表桶
{
if (bytes % align == 0)
return bytes / align - 1;
else
return bytes / align;
}
*/
//位运算写法
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
static inline size_t Index(size_t bytes) //确定桶
{
assert(bytes <= MAX_BYTES);
if (bytes <= 128)
{
//return _Index(bytes, 8);
return _Index(bytes, 3);
}
else if (bytes <= 1024)
{
//return _Index(bytes - 128, 16) + 16;
return _Index(bytes - 128, 4) + 16;
}
else if (bytes <= 8192)
{
//return _Index(bytes - 1024, 128) + 72; //16 + 56
return _Index(bytes - 1024, 7) + 72; //16 + 56
}
else if (bytes <= 65536)
{
//return _Index(bytes - 8192, 1024) + 128; //16 + 56 + 56
return _Index(bytes - 8192, 10) + 128; //16 + 56 + 56
}
else if (bytes <= 262144)
{
//return _Index(bytes - 65536, 8192) + 184; //16 + 56 + 56 + 56
return _Index(bytes - 65536, 13) + 184; //16 + 56 + 56 + 56
}
else
{
assert(false);
return -1;
}
}
// ⼀次从中⼼缓存获取多少个对象
static size_t NumMoveSize(size_t size)
{
if (size == 0)
return 0;
// [2, 512],⼀次批量移动多少个对象的(慢启动)上限值
// ⼩对象⼀次批量上限⾼
// ⼩对象⼀次批量上限低
int batchNum = MAX_BYTES / size;
if (batchNum < 2)
batchNum = 2;
if (batchNum > 512)
batchNum = 512;
return batchNum;
}
//计算一次向PageCache获取几个页
//单个对象 8byte
//...
//单个对象 256k
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT; //除以8k,也就是8 * 1024字节
if (npage == 0)
npage = 1;
return npage;
}
};
//管理多个连续页大块内存跨度结构
struct span
{
PAGE_ID _pageId = 0; //大块内存起始页的页号(比如一共有100页,这里的66就代表这是第66页的起始位置)
size_t _n = 0; //页的数量(比如3代表这个span有三个页)
span* _next = nullptr; //双链表结构,便于插入删除
span* _prev = nullptr;
size_t _useCount = 0; //切好小块内存,被分配给thread cache的计数
void* _freeList = nullptr; //切好的小块内存的自由链表
bool _isUse = false; //是否在被使用
};
class spanList
{
public:
spanList()
{
_head = new span;
_head->_next = _head;
_head->_prev = _head;
}
void Push_front(span* newspan)
{
Insert(Begin(), newspan);
}
span* Pop_front()
{
span* span = _head->_next;
Erase(span);
return span;
}
void Insert(span* pos, span* newspan) //插入
{
//span pos span -> span newspan pos span
assert(pos);
assert(newspan);
newspan->_next = pos;
newspan->_prev = pos->_prev;
pos->_prev->_next = newspan;
pos->_prev = newspan;
}
void Erase(span* pos) //删除
{
//span pos span -> span span
assert(pos);
assert(pos != _head);
pos->_next->_prev = pos->_prev;
pos->_prev->_next = pos->_next;
}
span* Begin()
{
return _head->_next;
}
span* End()
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
private:
span* _head;
public:
std::mutex _mutex; //桶锁
};
3.ConcurrentAlloc.h
cpp
#pragma once
#include "Common.h"
#include "ThreadCache.h"
//用户层
static void* ConcurrentAlloc(size_t size) //用户接口--申请内存
{
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
//std::cout << std::this_thread::get_id() << ":" << pTLSThreadCache << std::endl;
return pTLSThreadCache->Allocate(size);
}
static void ConcurrentFree(void* ptr, size_t size) //用户接口--释放内存
{
assert(pTLSThreadCache);
assert(ptr);
assert(size <= MAX_BYTES);
pTLSThreadCache->Deallocate(ptr, size);
}
4.PageCache.h
cpp
#pragma once
#include "Common.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
span* NewSpan(size_t size); //在PageList里面找有没有符合要求的_freeList的节点,没有就向系统申请一个大块内存
span* MapObjectToSpan(void* obj); //获取从对象到span的映射
void ReleaseSpanToPageCache(span* obj); //将CentralCache里面的span还回PageCache
private:
spanList _spanlist[MAX_PAGE_BUCKETS]; //定义128个桶
std::unordered_map<PAGE_ID, span*> _idSpanMap;
public:
std::mutex _pageMutex; //定义一个大锁而不是桶锁
private:
PageCache()
{ }
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
5.ThreadCache.h
cpp
#pragma once
#include "Common.h"
class ThreadCache
{
public:
void* Allocate(size_t size); //申请内存对象
void Deallocate(void* ptr, size_t size); //释放内存对象
void* FetchFromCentralCache(size_t index, size_t size); //向中心缓存获取对象
void ListTooLong(FreeList& list, size_t size); //释放对象的时候发现链表过长,就回收内存到中心缓存
private:
FreeList _freeList[MAX_BUCKETS]; //使用我们的分组方案,只需要分208组,就可以表示256kb以下情况
};
//_declspec(thread) 是MSVC(Windows)专属语法,标记变量为线程私有
//线程局部存储(TLS):_declspec(thread) 是为了让 pTLSThreadCache 成为线程私有变量,每个线程有独立副本,实现 ThreadCache 无锁访问
//个人解释:
//没有 _declspec(thread) 的问题:
//如果直接写 static ThreadCache* pTLSThreadCache = nullptr;
//这个指针是全局共用的------所有线程都用这一个指针指向同一个 ThreadCache(内存池)
//多线程同时分配 / 释放内存时,就得加锁保护这个内存池,一锁就慢,高并发下性能直接垮掉(这也是系统 malloc 慢的原因之一)。
//加了_declspec(thread) 的好处:
//这个关键字就是告诉编译器:"给每个线程都单独复制一份 pTLSThreadCache 指针,每个线程的指针只归自己用"
//比如线程 A 有自己的 pTLSThreadCache,指向线程 A 专属的内存池;线程 B 有自己的 pTLSThreadCache,指向线程 B 专属的内存池
//线程 A 和 B 操作自己的内存池时,完全不用管对方,不用加锁,速度直接拉满(这就是 TCMalloc "高并发" 的核心)
//*************************
//一句话,无锁操作就是让每个线程先用地盘专属的小内存池(ThreadCache),不用抢全局大池,所以不用加锁
//只有小池空了,才去全局大池批量拿内存(仅锁对应桶),整体几乎无锁
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
6.CentralCache.cpp
cpp
#include "CentralCache.h"
#include "PageCache.h"
CentralCache CentralCache::_sInst;
// 获取⼀个⾮空的span
span* CentralCache::GetOneSpan(spanList& list, size_t size)
{
span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
it = it->_next;
}
//这里有两种解锁方法
//1.把锁带入NewSpan()中
//2.现在就解锁
//我们选择2,如果只考虑申请,两种锁都可以,但是如果释放呢,那么第一种锁会让释放的线程拿不到锁而阻塞
//而第二种因为已经把锁释放了,所以可以完成线程释放
list._mutex.unlock();
//在外部加入page全局锁
PageCache::GetInstance()->_pageMutex.lock();
//走到这里说明没有非空span,需要向pageCache申请了
span* newspan = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
newspan->_isUse = true;
//完成函数解锁
//为什么切分的时候不需要这个锁呢,因为此时线程切分的内容只有自己访问得到,不涉及共享资源,所以不需要加锁
PageCache::GetInstance()->_pageMutex.unlock();
//计算span的大块内存起始地址以及内存大小(字节数) -> 加起来就是结束地址
//这里用char* 而不是void* 是因为char* 更适合字节加减(+1就是一字节)
char* start = (char*)(newspan->_pageId << PAGE_SHIFT); //相当于 * 1024 * 8 (8k)
size_t bytes = newspan->_n << PAGE_SHIFT;
char* end = start + bytes;
//把大块内存切成小块内存自由链表挂载到span->_freeList中
//1.先切一个小块作为头节点
newspan->_freeList = start;
start += size;
void* tail = newspan->_freeList;
//2.切剩下的,尾插(也可以使用头插,不过尾插可以保证物理内存线性连续)
while (start < end)
{
//头插
NextObj(tail) = start;
tail = NextObj(tail); //tail = start;
//后移
start += size;
}
//会访问共享资源,eg:
//线程一找list里面有没有span,而线程儿二正在插入span
//所以加上桶锁
list._mutex.lock();
list.Push_front(newspan); //头插到list中
return newspan;
}
// 从中⼼缓存获取⼀定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);
//加上桶锁避免线程竞争(互斥)
_spanList[index]._mutex.lock();
span* span = GetOneSpan(_spanList[index], size);
assert(span);
assert(span->_freeList);
start = span->_freeList;
end = start;
size_t i = 0;
size_t actualNum = 1;
while (i < batchNum - 1 && NextObj(end) != nullptr)
{
end = NextObj(end);
i++;
actualNum++;
}
span->_freeList = NextObj(end);
NextObj(end) = nullptr;
span->_useCount += actualNum;
_spanList[index]._mutex.unlock();
return actualNum;
}
//将一定数量的对象释放到span跨度
//为什么要给字节数呢,当然是要算在哪一个桶
//但是我们的start里面可能存在多个span的节点,所以要用到哈希表让每一个节点精准释放到size映射的桶里面
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanList[index]._mutex.lock();
while (start)
{
void* next = NextObj(start);
span* span = PageCache::GetInstance()->MapObjectToSpan(start); //获取从对象到span的映射
//将对应映射的start内存头插还给span
NextObj(start) = span->_freeList;
span->_freeList = start;
span->_useCount--;
//说明span的切分出去的所有块都已经还回来了,可以返还给PageCache了
if (span->_useCount == 0)
{
_spanList[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
_spanList[index]._mutex.unlock();
PageCache::GetInstance()->_pageMutex.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMutex.unlock();
_spanList[index]._mutex.lock();
}
start = next;
}
_spanList[index]._mutex.unlock();
}
7.PageCache.cpp
cpp
#include "PageCache.h"
PageCache PageCache::_sInst;
//在PageList里面找有没有符合要求的_freeList的节点,没有就向系统申请一个大块内存
span* PageCache::NewSpan(size_t size) //这里的size是页数
{
assert(size > 0 && size < MAX_PAGE_BUCKETS);
//先直接看size桶有没有
if (!_spanlist[size].Empty())
{
return _spanlist[size].Pop_front();
}
//size桶没有就往后找有的
//如果找到直接切下来size大小返回,i - size插入i - size桶
for (int i = size + 1; i < MAX_PAGE_BUCKETS; i++)
{
if (!_spanlist[i].Empty())
{
span* nspan = _spanlist[i].Pop_front();
span* kspan = new span;
//找页数大小为三的,发现100号桶有内存
//那么切除为3块 与 100 - 3块
//此时id 变为100与100 + 3,因为少了三块,往后移3块
kspan->_n = size;
kspan->_pageId = nspan->_pageId;
nspan->_n -= size;
nspan->_pageId += size;
_spanlist[nspan->_n].Push_front(nspan);
//存储nspan的首尾地址就可以(相当于挂接第103起始位置和对应的nspan)
_idSpanMap[nspan->_pageId] = nspan;
_idSpanMap[nspan->_pageId + nspan->_n - 1] = nspan;
//建立id和span的映射,方便CentralCache回收小块内存时,查找对应的span
for (size_t i = 0; i < kspan->_n; i++)
{
_idSpanMap[kspan->_pageId + i] = kspan;
}
return kspan;
}
}
//如果没有符合的,那么就向堆(系统)申请一个大块内存(128 * 8k)
span* bigspan = new span; //new只是创建一个span结构体对象(用来管理页的元数据,存_pageId、_n、_freeList等)
void* ptr = SystemAlloc(MAX_PAGE_BUCKETS - 1); //申请内存
bigspan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigspan->_n = MAX_PAGE_BUCKETS - 1;
_spanlist[bigspan->_n].Push_front(bigspan);
//为什么选择递归掉自己而不是直接切分返回呢
//1.现代计算机太快了,再遍历128次的时间损耗可以忽略不计
//2.在这个微乎其微的时间损耗下,我们更偏向于进行代码复用
return NewSpan(size);
}
span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
void PageCache::ReleaseSpanToPageCache(span* obj)
{
//向前合并
while (1)
{
PAGE_ID prevId = obj->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
if (ret == _idSpanMap.end())
{
break;
}
span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}
if (prevSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1)
{
break;
}
obj->_pageId = prevSpan->_pageId;
obj->_n += prevSpan->_n;
_spanlist[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
//向后合并
while (1)
{
PAGE_ID nextId = obj->_pageId + obj->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end())
{
break;
}
span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}
if (nextSpan->_n + obj->_n > MAX_PAGE_BUCKETS - 1)
{
break;
}
obj->_n += nextSpan->_n;
_spanlist[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
_spanlist[obj->_n].Push_front(obj);
obj->_isUse = false;
_idSpanMap[obj->_pageId] = obj;
_idSpanMap[obj->_pageId + obj->_n - 1] = obj;
}
8.ThreadCache.cpp
cpp
#include "ThreadCache.h"
#include "CentralCache.h"
//向中心缓存(central cache)申请内存对象
//申请到多个就先挂载(start->next到end, 因为start是要申请的内存结点)
//最终都返回start这个节点(一个节点,即申请的内存)
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
assert(index < MAX_BUCKETS);
assert(size <= MAX_BYTES);
//满开始反馈调节算法
//最开始申请就给一,之后_freeList[index].GetMaxSize()++
//最次也会保证即增长的同时也不会一次给太多
size_t batchNum = min(_freeList[index].GetMaxSize(), SizeClass::NumMoveSize(size)); //这里使用的是_WIN32下的宏min,而不是algorithm
if (batchNum == _freeList[index].GetMaxSize())
{
_freeList[index].GetMaxSize()++;
}
void* start = nullptr;
void* end = nullptr;
// 从中心缓存获取一定数量的对象给thread cache
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum > 0);
if (actualNum == 1) //只申请了一个,直接返回就可以
{
assert(start == end);
return start;
}
else //申请了多个,就需要先将[NextObj(start), end]挂到_freeList[index]中,再返回start
{
_freeList[index].rangePush(NextObj(start), end, actualNum - 1);
return start;
}
}
void* ThreadCache::Allocate(size_t size) //申请内存
{
size_t align = SizeClass::RoundUp(size); //对齐
size_t index = SizeClass::Index(size); //映射
if (_freeList[index].empty()) //为空,向中心缓存(central cache)申请内存对象
{
return FetchFromCentralCache(index, align);
}
else //有的话直接返回节点就行
{
return _freeList[index].pop();
}
}
void ThreadCache::Deallocate(void* ptr, size_t size) //释放内存
{
assert(ptr);
assert(size <= MAX_BYTES);
size_t index = SizeClass::Index(size); //映射
_freeList[index].push(ptr); //向指定映射的_freeList里面push
if (_freeList[index].Size() >= _freeList[index].GetMaxSize())
{
ListTooLong(_freeList[index], size); //太长了就还给CentralCache
}
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.rangePop(start, end, list.GetMaxSize()); //删掉list.GetMaxSize()个
CentralCache::GetInstance()->ReleaseListToSpans(start, size); //将一点数量的对象释放到span跨度
}