紧跟CentralCache设计之后
文章目录
PageCache整体设计
PageCache结构也是哈希桶结构,每个桶的数据都是Span链表。
- ThreadCache与CentralCache哈希桶的映射规律一致,PageCache哈希桶是根据页数分桶的。Span节点大小为一页的在哈希桶的第一个桶,Span大小为两页的在第二个桶依次类推
- CentralCache中Span节点被切分成小内存块给ThreadCache使用,而PageCache中的Span不切块。CentralCache向Page Cache 申请内存时关注的是Span的大小,PageCache中Span内部空间不需要切块。需要Span的大小直接对应了PageCache哈希桶的桶位置
注意:
当CentralCache向PageCache申请内存时,PageCache先检查对应位置有没Span,如果没有则向更大页寻找一个Span,如果找到则分裂成两个。
比如:申请的是4页PageCache,4页PageCache后面没有挂Span,则向后面寻找更大的Span,假设在10页Page位置找到一个Span,则将10页PageCache Span分裂为一个4页Page Span和一个6页Page Span。
CentralCache向PageCache申请空间,CentralCache是被多线程访问的,PageCache也是被多线程访问,所以PageCache也需要加锁。
注意: PageCache不能再使用桶锁,因为PageCache还要负责CentralCache内存回收工作。PageCache申请一页Span时如果Span链表没有就向两页大小Span链表中拿,所以不能设计成桶锁。
流程
开始时Page Cache桶中没有挂任何Span,此时申请2页Span时先向大页的桶中找Span切分。当大页都找不到时会向系统申请一个128页的Span,将这块空间放到PageCache对应的桶上。将128页切分成2页和126页,2页SpanCentralCache,126页挂到126页对应的桶上。此后要是再申请3页的Span,会依次向下找直到找到126页的Span,将Span差分成小页给CentralCache。
同理当CentralCache收回空间时,PageCache会将空间再合并成页用哈希桶管理,
如果CentralCache Span中的use_cout==0代表这个Span已经没有线程在使用了,将这个Span归还到对应PageCache哈希桶对应的位置,PageCache通过页号查看前后相邻页是否空闲,是的话就合并成更大的页,解决内存碎片问题
这里先考虑申请空间的过程
PageCache也是只有一个,设计成单例模式
C++代码
1.公共资源头文件(Common.h)Windows下直接向堆申请空间VirtualAlloc
注意:PageCache如果调用new向堆申请空间会报非法访问空间的错误,这里只能用VirtualAlloc直接向堆申请空间
#pragma once
#include<iostream>
#include<vector>
#include<time.h>
#include<assert.h>
#include<thread>
#include<mutex>
#include<algorithm>
#include<windows.h>
// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
using std::cout; using std::endl;
static const size_t MAX_BYTE = 126 * 1024;//如果线程申请超过126KB不能直接向ThreadCache申请空间
static const size_t NUMLIST = 208;//ThreadCache中哈希桶的个数
static const size_t NPAGE = 129;//PageCache中哈希桶的个数,0号桶空开从1号桶开始
static const size_t PAGESIZE = 13;//定义一页的大小为2^13(8K)
#ifdef _WIN64
typedef unsigned long long PAGE_ID;//64位下的页号
#elif _WIN32
typedef size_t PAGE_ID;
#else
//Linux
#endif // _WIN32
//获取自由链表下一个节点
static void*& NextObj(void* obj) {
return *(void**)obj;
}
//管理切分好内存的自由链表
class FreeList {
public:
FreeList() :_freeList(nullptr), MaxSize(1) {}
void Push(void* obj) {
//头插
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
}
void* Pop() {
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
return obj;
}
void PushList(void* begin, void* end) {
//头插链表
NextObj(end) = _freeList;
_freeList = begin;
}
bool Empty() { return _freeList == nullptr; }
size_t& GetMaxSize() { return MaxSize; }
private:
void* _freeList;
size_t MaxSize;//记录freeList链表要向中心缓存层申请多少个内存节点
};
//计算ThreadCache中哈希桶的桶个数
class SizeClass {
public:
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{
assert(false);
return -1;
}
}
//计算在哈希桶的那个位置
static inline size_t Index(size_t size) {
assert(size <= MAX_BYTE);
static int Group[4] = { 16,56,56,56 };
if (size <= 128) return _Index(size, 8);
else if (size <= 1024) return _Index(size - 128, 16) + Group[0];
else if (size <= 8 * 1024) return _Index(size - 1024, 128) + Group[1] + Group[0];
else if (size < 64 * 1024) return _Index(size - 8 * 1024, 1024) + Group[2] + Group[1] + Group[0];
else if (size < 256 * 1024) return _Index(size - 64 * 1024, 8 * 1024) + Group[3] + Group[2] + Group[1] + Group[0];
else {
assert(false);
return -1;
}
}
//ThreadCache一个Span中链表挂多少个空间节点
static size_t ForMemory(size_t size) {
assert(size > 0);
size_t num = MAX_BYTE / size;
if (num < 2) {
num = 2;//如果对象很大,一次少给ThreadCache一些。
}
else if (num > 512) {//如果对象很小,一次多给ThreadCache一些
num = 512;
}
return num;
}
//计算PageCache向系统申请页的数目
static size_t NumForPage(size_t size) {
size_t NumForMemory = ForMemory(size);//计算中心缓存层一个Span最多要多少节点
size_t Byte = NumForMemory * size;//总共的字节数
size_t NumPage = (Byte >> PAGESIZE);//计算申请几页
if (NumPage == 0) {
NumPage = 1;//至少给一页空间
}
return NumPage;
}
private:
static inline size_t _RoundUp(size_t size, size_t AlignNum) {
size_t AligSize = size;
if (size % AlignNum != 0) {
AligSize = (size / AlignNum + 1) * AlignNum;
}
return AligSize;
}
static inline size_t _Index(size_t size, size_t AlignNum) {
size_t Pos = 0;
if (size % AlignNum == 0) {
Pos = size / AlignNum - 1;
}
else {
Pos = size / AlignNum;
}
return Pos;
}
};
//管理以页为单位的大块空间结构
struct Span {
PAGE_ID _PageID;//记录是第几页
size_t _Num;//记录Span里面有多少页
Span* _next;//双向链表
Span* _prev;
size_t use_count;//记录分配了多少个对象给ThreadCahce
void* FreeList;//切好的小块内存空间
Span() :_PageID(0), _Num(0), _next(nullptr), _prev(nullptr), use_count(0), FreeList(nullptr) {}
};
class SpanList {
private:
Span* _head;
public:
std::mutex _mtx;//桶锁
SpanList() {//带头双向循环链表
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
void Insert(Span* pos, Span* NewSpan) {
//pos位置前插入NewSpan
assert(pos != nullptr && NewSpan != nullptr);
Span* prev = pos->_prev;//前一个
prev->_next = NewSpan;
NewSpan->_prev = prev;
NewSpan->_next = pos;
pos->_prev = NewSpan;
}
Span* Pop() {
Span* front = _head->_next;//带头双向循环链表
Erase(_head->_next);
return front;
}
void Erase(Span* pos) {//删除SpanList上的节点
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
Span* begin() { return _head->_next; }
Span* end() { return _head; }
bool Empty() { return _head->_next == _head; }
};
2.线程缓存层实现(ThreadCache.h)
#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);
}
//向中心缓存申请空间
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);//将链表的下一个节点插入桶中,头节点返回给ThreadCache
return begin;
}
}
};
//每个线程都会拥有自己的ThreadCache
static __declspec(thread) ThreadCache* tls_threadcache = nullptr;//用static修饰只在当前文件可见
线程申请独立申请ThreadCache(ConcurrentAlloc.h)
#pragma once
#include"Common.h"
#include"ThreadCache.h"
//线程调用申请ThreadCache空间
static void* ConcurrentAlloc(size_t size) {
//获取线程自己的ThreadCache
if (tls_threadcache == nullptr) {
tls_threadcache = new ThreadCache;
}
cout << std::this_thread::get_id()<<" "<<tls_threadcache<< endl;
return tls_threadcache->ApplySpace(size);
}
static void ConcurrentFree(void* ptr, size_t size) {
//释放时每个线程一定有tls_threadcache
assert(tls_threadcache != nullptr);
tls_threadcache->ReleaseSpace(ptr,size);
}
3.中心缓存层实现(CentralCache.h / CentralCache.cpp)
#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);
};
#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));
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;
}
4.页缓存实现过程(PageCache.h / PageCache.cpp)
#pragma once
#include"Common.h"
class PageCache {
private:
SpanList _SpanList[NPAGE];
static PageCache _sInst;
PageCache() {}
public:
std::mutex _PageMtx;
PageCache(const PageCache&) = delete;
static PageCache* GetInst() {
return &_sInst;
}
//获取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);
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);
}
申请内存测试
#include"ConcurrentAlloc.h"
void TestConcurrentAlloc() {
void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(7);
void* p5 = ConcurrentAlloc(18);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
cout << p4 << endl;
cout << p5 << endl;
}
int main()
{
TestConcurrentAlloc();
return 0;
}
PageCache第二次申请128页空间测试
#include"ConcurrentAlloc.h"
void TestConcurrentAlloc() {
void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(7);
void* p5 = ConcurrentAlloc(18);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
cout << p4 << endl;
cout << p5 << endl;
}
void TestConcurrentAlloc2() {
for (size_t i = 0; i < 1024; i++) {
void* p1 = ConcurrentAlloc(6);
cout << p1 << endl;
}
//第二次申请128页
void* p2 = ConcurrentAlloc(6);
cout << p2 << endl;
}
int main()
{
//TestConcurrentAlloc();
TestConcurrentAlloc2();
return 0;
}
项目走到这里申请空间过程基本没有什么太大的问题了,接下来就是释放空间的过程