简单介绍相关概念
首先要引入进程,线程,互斥锁这几个概念,我们用生活中的例子来举例。进程就好比工厂中的车间。一个车间可以有很多工人,又要引入一个新的概念,线程,线程好比一个个工人,他们共同完成一个任务。这就意味着一个进程可以包括很多线程。进程空间是被线程共享的。 这时候要引入一个新的概念互斥锁,好比工厂中一些特殊的房间,当在被使用时就要上锁以防止其他人的访问,当不用时又要解锁,线程也是同理。
项目介绍
这个项目一共会创建如下文件:
Common.h
ThreadCache.h
ObjectPool.h
ConcurrentAlloc.h
CentralCache,h
PageCache.h
PageMap.h
ThreadCache.cpp
UnitTest.cpp
CentralCache.cpp
PageCache.cpp
Benchmark.cpp
(下面的代码是在讲解中不断优化添加的)
项目基础:C/C++,数据结构,线程理解,线程锁
什么是内存池
高并发内存池说简单点就是可以在多线程情况下的内存管理相较于malloc更加高效。现在知名的原型就是谷歌开源项目tcmalloc,我们将要实现的是一个简化版。实现对malloc/free相关函数的替换。
首先我们要提到池化技术,我们可能在很多领域都听说过池化技术,比如说在图像识别中,是将图像变成更小的特征图,但却保存原有的图像信息。在内存池中的池化技术是一种内存管理技术,其向操作系统申请一块较大的内存,当程序需要内存时直接向内存池中申请,而不是向操作系统中申请,当程序释放内存时,并不是直接返回给操作系统,而是返还给内存池。除了内存池像对象池,线程池等也是相似原理。
1.因为频繁的申请内存,会多次进行系统调用,就会涉及用户态到内存态再到用户态的转换,开销巨大,内存池就可以实现在用户态对内存进行管理。
2.频繁的向操作系统进行内存申请和释放,会导致不少的很小的内存未被有效利用的情况,因为这些小内存可能是不连续的,不能直接合成一块较大内存进行利用,导致内存碎片的问题。内存池就可以有效解决这些问题。(内存碎片也分内碎片和外碎片,会在下面进行解释,这里提到的就是外碎片)
我们可以将内存池与生活中的一些现象联系起来,比如说买零食,我们可以选择一次只买一点,有需要的时候又自己去超市买,但是这样就会花费不少时间在赶往超市,第二种方法就是第一次就买自己未来一个月所需要的零食,放在家里,这样每次去的时候就方便快速很多。 高并发内存池在多线程的情况下更有优势。
首先实现一个定长内存池
我们要先知道一个单位换算1kb=1024b,1b=8比特。
在我们学习动态链表之前肯定是先了解静态链表,这里我们也先实现一个定长内存池。
这个char* _memmory是指向大块的内存池,这里为什么设置为char*,因为若设置为int*或其它类型不方便管理,char*是一字节,方便进行移动定位(因为要定位分出去后剩下内存池的起始地址)。若被分出去的内存进行了释放归还时应用一个_freeList自由链表先进行管理,用前一个内存块前面四个字节(32位平台)储存后面一个内存的地址方便连接
在内存对象归还给自由链表连接过程中,可能存在空和非空两种情况,但是合理运用头插可用相同代码处理。
第一张是自由链表还是空的情况,第二张是已经悬挂节点(返回了内存对象)的情况。
obj->~T();//显示调用析构函数清理对象
//这里进行头插可以兼顾自由链表为空的情况
*(void**)obj = _freeList;
_freeList = obj;
这个代码十分关键,void**二级指针解引用是看一级指针的大小,32位下是4字节,64位下是8字节,就可以直接处理不同向后移动空间的情况。 (只要是二级指针都可以)
当大块内存没有内存对象时会优先向自由链表申请,让自由链表切出相应大小的内存对象,若自由链表也没有足够大小的内存时,可以直接向系统申请内存,windows直接向内存申请内存的函数VirtualAlloc。
ObjectPool.h
#pragma once
#include <iostream>
#include<vector>
#include<time.h>
using std::cout;
using std::cin;//部分展开std
//下面要处理在不同环境下编译的情况
#ifdef _WIN32
#include<windows.h>
#else
//
#endif
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 0;
}
template<class T>//表示这个内存池每次都是获取一个T对象
class ObjectPool
{
public:
T* New()//New个T的对象
{
T* obj = nullptr;//记录拿走内存的地址
if (_freeList)//先把还回来的内存优先利用
{
void* next = *((void**)_freeList);//这里储存的是地址
obj =(T*) _freeList;
_freeList = next;
}
else
{
if (_remainBytes < sizeof(T))//最终分出去剩下的内存不够给一个对象T,直接不要了,重新开大块空间
{
_remainBytes=128 * 1024;//若一开始没有内存先分给128kb内存
_memory = (char*)malloc(_remainBytes);
if (_memory == nullptr)//处理开空间失败
{
printf("malloc is fail");
exit(-1);
//throw bad_alloc();//C++的抛异常
}
}
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*): sizeof(T);
_memory +=objSize;//这里不能+=sizeof(obj)因为obj是指针,固定大小是4或者8字节。T的作用就是确定多少内存储存一个新对象
_remainBytes -= sizeof(T);//每用一次就要减一下
}
new(obj)T;//显示调用初始化
return obj;
}
void* Delete(T* obj)
{
obj->~T();//显示调用析构函数清理对象
//这里进行头插可以兼顾自由链表为空的情况
*(void**)obj = _freeList;
_freeList = obj;
return obj;
}
private:
char* _memory = nullptr;//因为最开始的状态是_memmory,_freeList都是空
size_t _remainBytes = 0;//指向大块内存被分出去剩余的字节数
void* _freeList = nullptr;//还回来时先挂在自由链表上
};
//还有一段这个测试效率的代码,并不影响运行
在release情况下
可以看出内存池效率明显有提高
高并发内存池整体框架设计
在多线程编程下,锁竞争是使性能过多消耗的常见问题。如何优化锁竞争是高并发内存池的目标
总体分为三个大部分
thread cache(线程缓存):用于小内存的分配,有一个线程就要创建一个thread cache ,所以说每一个线程都会独享一个cache,这样线程在这里申请内存的时候就不用加锁,大大提高效率。
central cache(中心缓存);因为中心缓存是共享的,所以thread cache内存不够的时候可以来这里申请内存。中心缓存的结构是哈希桶,在central cache中是存在桶锁的。只有当不同线程因为内存不够访问同一个桶的时候才会存在锁竞争,所以说并不会很激烈。
page cache(页缓存):当CentralCache没有内存对象时。PageCache会分出一定数量的page,切割成小块内存,分给CentralCache,当满足一定条件后,PageCache会回收CentralCache满足条件的span对象,组成更大的页,处理内存碎片问题。
ThreadCache整体设计
我们这里考虑的内存大小通常时256kb及以下。
我们上面介绍的就是一个256kb以内情况下定长内存池的处理,用了一种自由链表,但是却只能解决某一字节数的内存池,还会有其它字节数的长度的情况,所以说我们要考虑搞多个自由链表,采用的哈希桶的结构,但是如果我们每个字节数都考虑弄一个自由链表,消耗就太大了,所以我们要考虑平衡的牺牲一下一定内存。进行如下处理,分别依次挂8,16,24,......,256(如果小于8,用8字节。在8到16之间挂16字节。以此类推),这样导致的浪费就是内碎片。当被申请内存时,首先看对应大小的桶上是否悬挂有自由链表,有的话就取,没有的话则向中心缓存申请。
接下来我们先把ThreadCache.h的相关函数申明写好,再完成自由链表的常见操作。(如头上头插一个内存对象,头上取一个内存对象)
Common.h
#pragma once
#include <iostream>
#include<vector>
#include<time.h>
#include<assert.h>
using std::cout;
using std::cin;//部分展开std
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
public:
void Push(void* obj)
{
//头插
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
}
void* Pop()//拿出头上这个对象
{
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
return obj;
}
bool Empty()
{
return _freeList == nullptr;
}
private:
void* _freeList= nullptr;
};
ThreadCache.h
#include"Common.h"
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);//释放时既要知道释放的地址,又要知道大小,因为才知道映射在哪个桶
private:
FreeList _freeList[];//根据映射关系创建数组
};
ThreadCache哈希桶映射对齐规则
从上面可以看出Thread Cache是哈希桶结构,每来一个大小都要一个桶进行映射,上面我们说过在一个范围内都给一个固定大小,减少消耗。如果我们申请一个内存大小,ThreadCache要给多少大小给我们呢?我们用一个SizeCalss这个类来管理映射规则。我们至少要用八字节用来对齐,因为我们需要一部分内存大小来储存下一个节点地址(地址在不同情况下分别占四字节或八字节)
我们为了降低消耗采取以下对齐方式(若为130字节,我们采取16字节对齐,所以我们要144字节,浪费144-130=14字节)
// [1,128] 8byte对齐 freelist[0, 16)桶的数量
// [128+1,1024] 16byte对齐 freelist[16, 72)
// [1024+1,8*1024] 128byte对齐 freelist[72, 128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128, 184)
// [64*1024+1,256*1024] 8 * 1024byte对齐 freelist[184, 208)
Common.h
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
实现找到向上对齐的字节数
static inline size_t _Index(size_t bytes, size_t align_shift)
找到哈希桶对应坐标
#pragma once
#include <iostream>
#include<vector>
#include<time.h>
#include<assert.h>
using std::cout;
using std::cin;//部分展开std
static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREE_LIST =208;//208个桶
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
public:
void Push(void* obj)
{
//头插
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
}
void* Pop()
{
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
return obj;
}
bool Empty()
{
return _freeList == nullptr;
}
private:
void* _freeList= nullptr;
};
//下面是由tcmalloc简化而来的映射规则
class SizeClass//计算对象大小的对齐映射规则
{
public:
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0, 16)桶的数量
// [128+1,1024] 16byte对齐 freelist[16, 72)
// [1024+1,8*1024] 128byte对齐 freelist[72, 128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128, 184)
// [64*1024+1,256*1024] 8 * 1024byte对齐 freelist[184, 208)
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
/*
size_t alignSize;
if(bytes%alignNum!=0)
{alignSize=(bytes/alignNum+1)*alignNum;}//例如是7字节,7/8=0,0+1=1,1*8=8
else
{alignSize=bytes;}
return alignSize;
*/
return (((bytes)+alignNum - 1) & ~(alignNum - 1));//高级方法
}
// 对齐大小计算
static inline size_t RoundUp(size_t bytes)
{
if (bytes <= 128) {
return _RoundUp(bytes, 8);
}
else if (bytes <= 1024) {
return _RoundUp(bytes, 16);
}
else if (bytes <= 8 * 1024) {
return _RoundUp(bytes, 128);
}
else if (bytes <= 64 * 1024) {
return _RoundUp(bytes, 1024);
}
else if (bytes <= 256 * 1024) {
return _RoundUp(bytes, 8 * 1024);
}
else
{
assert(false);
//return _RoundUp(bytes, 1 << PAGE_SHIFT);
return -1;
}
return -1;
}
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);
static int group_array[4] = { 16, 56, 56, 56 };//前几个范围有几个桶
if (bytes <= 128) {
return _Index(bytes, 3);
}
else if (bytes <= 1024) {
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 8 * 1024) {
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024) {
return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1]
+ group_array[0];
}
else if (bytes <= 256 * 1024) {
return _Index(bytes - 64 * 1024, 13) + group_array[3] +
group_array[2] + group_array[1] + group_array[0];
}
else {
assert(false);
}
return -1;
}
};
ThreadCache.cpp
#include"ThreadCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
return nullptr;
}
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size);
size_t index = SizeClass::Index(size);
if (!_freeList[index].Empty())//如果自由链表有东西则直接取,没有对象则向下一层申请
{
return _freeList[index].Pop();
}
else
{
return FetchFromCentralCache(index, alignSize);
}
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 还给桶的时候要知道大小才知道还给那个桶
size_t index = SizeClass::Index(size);
_freeList[index].Push(ptr);
}
ThreadCache.h
#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);//从中心缓存获取对象
private:
FreeList _freeList[NFREE_LIST];//根据映射关系创建数组
};
ThreadCacheTLS无锁访问
线程局部存储(TLS),是一种变量的存储方法,每一个线程都有一个ThreadCache,线程内的变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。
每一个线程都有一个这个变量
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
接下来用代码实现每一个线程申请和释放,通过每一个线程拥有一个ThreadCache来避免锁的使用
ConcurrentAlloc.h
#pragma once
#include "Common.h"
#include "ThreadCache.h"
static void* ConcurrentAlloc(size_t size)
{
// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;
return pTLSThreadCache->Allocate(size);
}
static void ConcurrentFree(void* ptr, size_t size)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
ThreadCache.cpp
#include"ThreadCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index,size_t size)
{
return nullptr;
}
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size);
size_t index = SizeClass::Index(size);
if (!_freeList[index].Empty())
{
return _freeList[index].Pop();
}
else
{
return FetchFromCentralCache(index, alignSize);
}
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 还给桶的时候要知道大小才知道还给那个桶
size_t index = SizeClass::Index(size);
_freeList[index].Push(ptr);
}
ThreadCache.h
#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);//从中心缓存获取对象
private:
FreeList _freeList[NFREE_LIST];//根据映射关系创建数组
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;//_declspec(thread)是一个特有的拓展,用于申明一个变量的局部存储,若有三个线程就有三个pTLSThreadCache变量
UnitTest.cpp
#include"ObjectPool.h"
#include "ConcurrentAlloc.h"
void Alloc1()
{
for (size_t i = 0; i < 5; ++i)
{
void* ptr = ConcurrentAlloc(5);
}
}
void Alloc2()
{
for (size_t i = 0; i < 5; ++i)
{
void* ptr = ConcurrentAlloc(7);
}
}
void TLSTest()
{
//这样写可以看到每个线程都有自己的tls
std::thread t1(Alloc1);//这是库里面的调用方法。创建一个线程调用
t1.join();
std::thread t2(Alloc2);
t2.join();
}
int main()
{
//TestObjectPool();
TLSTest();
return 0;
}
前面是线程号后面是地址
这样可以看出每个线程都有自己独立的TLS。
CentralCache的整体设计
从ThreadCache内存不够时会向中心缓存申请内存,CentralCache也是哈希桶结构不过却与ThreadCache有一些区别,ThreadCache上挂的是一系列内存块,CentralCache的每个哈希桶位是挂的是一个SpanList链表结构,这意味着在这个链表里,存储的是一系列的Span(以页为单位的大块内存),例如8Byte映射位置下面挂的span中的页被切成8Byte大小的对象的自由链表。16KB位置的span中的页被切成16KB大小对象的自由链表,CentralCache映射的spanlist中所有span的都没有内存以后,则需要PageCache申请一个新的span对象.拿到spanl以后将span管理的内存技大小切好作为自由链表链接到一起。若ThreadCache向中心缓存申请内存,中心缓存从span中取对象给ThreadCache。不同字节对应的自由链表所在的桶存在桶锁,CentralCache中挂的span中use_count记录分配了多少个对象出去,分配一个对象给ThreadCache,就++use_count。释放内存,当ThreadCache过长或者线程销毁,则会将内存释放回CentralCache中的,释放回来时-use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回Page Cache,PageCache中会对前后相邻的空闲页进行合并。
CentralCache结构设计
主要实现用带头双向循环链表挂span
Common.h
//包含各种头文件
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#endif
//省略之前的代码
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
PAGE_ID _pageId=0;//大块内存起始页的页号
size_t _n=0;//页的数量
Span* _next=nullptr;//双向链表的结构
Span* _prev = nullptr;
size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
void* _freeList = nullptr;//切好的小块内存的自由链表
};
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
std::mutex _mtx;//桶锁
};
CentralCache.h
#pragma once
#include "Common.h"
class CentralCache
{
private:
SpanList _spanLists[NFREE_LIST];//这里的结构和ThreadCache相似
};
CentralCache的核心实现
这里ThreadCache没有内存的时候会不断向CentralCache申请内存,虽然存在桶锁,但是一般是不同的桶锁,竞争还不算大,但是若申请同一个锁,竞争情况就相对较大,效率就会损失。我们可以通过一次多给一些内存来解决这个问题。避免反复进行锁的相关操作。但是一次性给多少内存呢,若要一个一次性给十个或多个,则用完一个,申请后面九个或多个的时候就是无锁的情况。在这种情况下也可能存在不够九个或者多个的情况,这种情况下是有多少个拿多少个,因为目标只是需要一个内存对象,多要的只是方便给ThreadCache后,需要的内存对象可以直接在ThreadCache中申请,避免过多的消耗,这种多给的方法会存在用不完浪费的情况。所以较官方的方法用了一个慢开始反馈调节算法。
慢开始反馈调节算法:还是开始给一个批量内存,大的对象少给一点,小的对象多给一点
Common.h
//......
// 一次ThreadCache从CentralCache获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);//size单个对象大小
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
};
//......
ThreadCache.cpp
注意这行代码的理解
min(_freeList[index].MaxSize(),SizeClass::NumMoveSize(size));
一开始不会向CentralCache批量要太多,因为要太多可能用不完,如果不断有size大小的内存的需求,那么batchNum就会不断增长,直到上限
size越大,一次向central cache要的batchNum就越小.size越小,一次向central cache要的batchNum就越大
#include"ThreadCache.h"
#include"CentralCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index,size_t size)
{
//慢开始的调节算法
//一开始不会向CentralCache批量要太多,因为要太多可能用不完,如果不断有size大小的内存的需求,那么batchNum就会不断增长,直到上限
//size越大,一次向central cache要的batchNum就越小.size越小,一次向central cache要的batchNum就越大
size_t batchNum = std::min(_freeList[index].MaxSize(),SizeClass::NumMoveSize(size));
if (_freeList[index].MaxSize() == batchNum)
{
_freeList[index].MaxSize() += 1;
}
void* start = nullptr;
void* end = nullptr;
size_t actualNum=CentralCache::GetInstance()->FetchRangeObj(start,end,batchNum,size);
assert(actualNum >= 1);
if (actualNum==1)
{
assert(start == end);
return start;
}
else
{
_freeList[index].PushRange(NextObj(start), end);
return start;
}
}
CentralCache.cpp
#include"CentralCache.h"
CentralCache CentralCache::_sInst;
Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)//获取一个非空Span
{
//...
return nullptr;
}
//从中心缓存获取一定数量的对象给Thread Cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);//算出对应在桶什么位置
_spanLists[index]._mtx.lock();
Span* span = GetOneSpan(_spanLists[index], size);
assert(span);
assert(_freeList);
// 从span中获取batchNum个对象
// 如果不够batchNum个,有多少拿多少
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;
_spanLists[index]._mtx.unlock();
return actualNum;
}
CentralCache.h
#pragma once
#include "Common.h"
// 单例模式
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;//获取实例对象
}
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t byte_size);
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
private:
SpanList _spanLists[NFREE_LIST];//这里的结构和ThreadCache相似
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
};
Common.h
//......省略上面的代码
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
PAGE_ID _pageId=0;//大块内存起始页的页号
size_t _n=0;//页的数量
Span* _next=nullptr;//双向链表的结构
Span* _prev = nullptr;
size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
void* _freeList = nullptr;//切好的小块内存的自由链表
};
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx;//桶锁
};
PageCache整体设计
span是管理多个对象的页。当CentralCache没有span或空闲的span,则要向下一层取对象,PageCache也是哈希桶,对应位置挂的也是一个个span,但是却有不一样,ThreadCache和ThreadCache映射规则都一样,但是PageCache不一样,它挂有128页span,每个页对应一个桶,中心缓存中把页切成小对象给ThreadCache用,用不完可以还给中心缓存,但PageCache不进行切分,中心缓存向页缓存要span的时候要先算好要几页span,又要找到这个span在页缓存的哪个桶,直接给,又因为每一个页对应一个桶所以相对容易找到,也容易还。
我们可以从上面的图看出来,最大的是128page,为什么最大的是128page呢?从上面可知,我们假设最大申请的单个对象是256kb,128*8k=1024kb=4*256kb,可以看出来对于是单个对象256kb的四倍,完全够用了。只要能满足自己的需求想设置为多少就设置为多少。
PageCache不是桶锁,因为与下面的分裂与合并有关系。当进行跨桶进行合并与切割时,会进行反复的上锁与解锁的操作,大大加大性能消耗要用一个大锁锁起来。
这里会存在一个问若中心缓存向页缓存申请两页的span,若两页的位置没有span,并不是直接向堆申请,而是会向大页申请span,切出两页的span。若后面没有一个挂span,则向系统(堆)申请一个128页的span,挂到对应位置,然后切出一个两页的span拿给中心缓存用,再把剩余的126页span挂到对应位置。
当所以内存还回来的时候要进行合并,如果中心缓存中的span _useCount等于0说明切中心缓存给他、ThreadCache小快内存都回来了,则中心缓存把这个span还给页缓存,页缓存通过页号,查看前后的相邻页是否空闲,是的话就合并,合并出更大的页,解决内存碎片问题。
static const size_t NPAGES = 129;//大小调大一个,方便后面下标处理
PageCache.h
#pragma once
#include "Common.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;//获取实例对象
}
Span* Newspan(size_t k);//获取一个k页的span
private:
SpanList _sapnLists[NPAGES];
std::mutex _pageMtx;//大锁
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
PageCache.cpp
#include"PageCache.h"
PageCache PageCache::_sInst;
PageCache中获取span上
先继续完善带头双向循环链表
common.h
//......
class FreeList
{
//......
// 一次ThreadCache从CentralCache获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 计算一次向系统获取几个页
// 单个对象 8byte
// ...
// 单个对象 256KB
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
PAGE_ID _pageId=0;//大块内存起始页的页号
size_t _n=0;//页的数量
Span* _next=nullptr;//双向链表的结构
Span* _prev = nullptr;
size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
void* _freeList = nullptr;//切好的小块内存的自由链表
};
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
Span* Begin()//遍历
{
return _head->_next;
}
Span* End()
{
return _head;
}
void PushFront(Span* span)//头插
{
Insert(Begin(), span);
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx;//桶锁
};
Span* CentralCache::GetOneSpan(SpanList& list, size_t size){
//......
}
先从这个函数引入相关内容,ThreadCache向中心缓存申请内存对象
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 查看当前的spanlist中是否有还有未分配对象的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
list._mtx.unlock();
// 走到这里说没有空闲span了,只能找page cache要
//......
走到这里说没有空闲span了,只能找PageCache要。
PageCache中获取span下
static const size_t PAGE_SHIFT = 13;
这个代表相关页的转换除以8k等于>>13位
//计算一次向系统获取几个页
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
通过这个函数算出页数。接下来GetOneSpan的逻辑在CentralCache.cpp中接着实现。
为了实现在不同环境下编译,我们进行条件编译
Common.h
#include <assert.h>
#include <thread>
#include <mutex>
#include <atomic>
using std::cout;
using std::endl;
#ifdef _WIN32
#include <windows.h>
#else
// ...
#endif
static const size_t MAX_BYTES = 256 * 1024;
static const size_t NFREELIST = 208;
static const size_t NPAGES = 129;
static const size_t PAGE_SHIFT = 13;
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#else
// linux
#endif
//......
class FreeList
{
//......
// 一次ThreadCache从CentralCache获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 计算一次向系统获取几个页
// 单个对象 8byte
// ...
// 单个对象 256KB
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
//管理多个连续页的大块内存跨度,放common.h因为CentralCache和PageCache都要用
struct Span
{
PAGE_ID _pageId=0;//大块内存起始页的页号
size_t _n=0;//页的数量
Span* _next=nullptr;//双向链表的结构
Span* _prev = nullptr;
size_t _useCount=0;//切好的小块内存分给ThreadCache的计数
void* _freeList = nullptr;//切好的小块内存的自由链表
};
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
Span* Begin()//遍历
{
return _head->_next;
}
Span* End()
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
Span* PopFront()//取出来
{
Span* front = _head->_next;
Erase(front);
return front;
}
void PushFront(Span* span)//头插
{
Insert(Begin(), span);
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx;//桶锁
};
char* start = (char*)(span->_pageId << PAGE_SHIFT);//找到页的起始地址,左移是乘
size_t bytes = span->_n << PAGE_SHIFT;//计算大块内存的字节数
这里是通过页号算出起始地址。 又得到单个的大小,就可以大块内存切成自由链表挂起来。
CentralCache.cpp
#include"CentralCache.h"
#include"PageCache.h"
CentralCache CentralCache::_sInst;
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)//获取一个非空Span
{
//查看当前的spanlist中是否还有未分配对象的span
Span* it = list.Begin();
while (it != list.End())//使用迭代器遍历
{
if (it->_freeList != nullptr)
{
return it;
}
else {
it = it->_next;
}
}
//先把中心缓存的桶锁解掉,这样如果其它线程释放内存对象回来不会阻塞
list._mtx.unlock();
//走到这里说明没有空闲的span,只能找页缓存
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
PageCache::GetInstance()->_pageMtx.unlock();
//对获取的span切分,不需要加锁,因为其它线程拿不到span
//处理切分逻辑
char* start = (char*)(span->_pageId << PAGE_SHIFT);//找到页的起始地址,左移是乘
size_t bytes = span->_n << PAGE_SHIFT;//计算大块内存的字节数
char* end = start + bytes;
//把大块内存切成自由链表链接起来
//先切一块下来去做头,方便尾插
span->_freeList = start;
start += size;
void* tail = span->_freeList;
int i = 1;
while (start < end)
{
++i;
NextObj(tail) = start;
tail = NextObj(tail); // tail = start;
start += size;
}
NextObj(tail)=nullptr;
//切好span后,需要把span挂到桶里面去的时候,再加锁
list._mtx.lock();
list.PushFront(span);
return span;
}
//从中心缓存获取一定数量的对象给Thread Cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);//算出对应在桶什么位置
_spanLists[index]._mtx.lock();
Span* span = GetOneSpan(_spanLists[index], size);
assert(span);
assert(_freeList);
// 从span中获取batchNum个对象
// 如果不够batchNum个,有多少拿多少
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;
_spanLists[index]._mtx.unlock();
return actualNum;
}
因为在PageCache中是通过页数作为桶号进行映射获取span的,所以是几号桶就获取几页的span.在PageCache获取span给中心缓存,若获取两页的span则在页缓存两页的位置找,若找的到则头删出去拿出来,没有就往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去用,剩下的一页挂在一页的位置上。在NewSpan函数中实现
PageCache.cpp
#include"PageCache.h"
PageCache PageCache::_sInst;
//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
if (!_spanLists[k].Empty())//先检查第k个桶有没有span
{
return _spanLists->PopFront();//拿出来
}
//检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
for (size_t i = k + 1; i < NPAGES; ++i)//从下一页开始找
{
if (!_spanLists[i].Empty())
{
//开始切
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
//处理剩下页数的span
_spanLists[nSpan->_n].PushFront(nSpan);
return kSpan;
}
}
//走到这个位置说明后面没有大页的span了,这时向系统(堆)要一个128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//有地址算页号就是除
bigSpan->_n =NPAGES-1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
return NewSpan(k);
这里类似递归重新调用了自己,但不会进入死循环,因为这里至少有了128页可以进行切分,就会在for循环里面找满足调节,不会再一次调用NewSpan()了。
PageCache.h
#pragma once
#include "Common.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;//获取实例对象
}
Span* NewSpan(size_t k);//获取一个k页的span
std::mutex _pageMtx;
private:
SpanList _spanLists[NPAGES];//NPAGES=128
PageCache()
{
}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
UnitTest.cpp
#include"ObjectPool.h"
#include "ConcurrentAlloc.h"
void TestConcurrentAlloc()
{
void* p1 = ConcurrentAlloc(6);
void* p2= ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(7);
void* p5 = ConcurrentAlloc(8);
cout << p1 << std::endl;
cout << p2 << std::endl;
cout << p3 << std::endl;
cout << p4 << std::endl;
cout << p5 << std::endl;
}
int main()
{
//TestObjectPool();
//TLSTest();
TestConcurrentAlloc();
return 0;
}
这是调用一个并发内存分配的操作,得到内存地址。
可以看出来这些申请的小块内存是连续的
ThreadCache回收内存
首先因为每一个线程都有一个ThreadCache,所以大多数都是无锁操作,使其更加高效。当线程中某些对象不再使用时会返回到ThreadCache中,再挂到对应的哈希桶上,当某个桶上的自由链表因为释放对象不断增加到某个大小时,就会批量返回给中心缓存的span。
ThreadCache.h
添加
void ListTooLong(FreeList& list, size_t size);
ThreadCache释放内存会挂在自由链表上,当自由链表过长时会回收内存到中心缓存
ThreadCache .cpp
//......
void* ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 还给桶的时候要知道大小才知道还给那个桶
size_t index = SizeClass::Index(size);
_freeList[index].Push(ptr);
if (_freeList[index].Size()>=_freeList[index].MaxSize())//当链表长度大于一次批量申请的内存时就开始还一段list给中心缓存
{
ListTooLong(_freeList[index], size);
}
}
void* ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//取一批内存回来还给下一层
list.PopRange(start, end, list.MaxSize());//取一次批量的内存
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
//......
CentralCache.h
添加
// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
Common.h
PopRange图示
//......
class FreeList
{
public:
void Push(void* obj)
{
//头插
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
void PushRange(void* start, void* end,size_t n)//给一个范围给多个对象
{
NextObj(end) = _freeList;
_freeList = start;
_size += n;
}
void PopRange(void*& start, void*& end, size_t n)//注意给了引用
{
assert(n >= _size);
start = _freeList;
end = start;
for (size_t i = 0; i < n-1; i++)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
void* Pop()
{
//头删
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
--_size;
return obj;
}
bool Empty()
{
return _freeList == nullptr;
}
size_t& MaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList=nullptr;
size_t _maxSize = 1;
size_t _size;//记录数据个数
};
//......
CentralCache回收内存
首先ThreadCache中自由链表回收的内存可能是不同对象还回来的。我们要知道哪个对象属于哪个页。
UnitTest.cpp
//......
void TestAddressShift()
{
PAGE_ID id1 = 2000;
PAGE_ID id2 = 2001;
char* p1 = (char*)(id1 << PAGE_SHIFT);
char* p2 = (char*)(id2 << PAGE_SHIFT);
while (p1 < p2)
{
cout << (void*)p1 << ":" << ((PAGE_ID)p1 >> PAGE_SHIFT) << std::endl;
p1 += 8;
}
}
int main()
{
//TestObjectPool();
//TLSTest();
//TestConcurrentAlloc1();
TestAddressShift();
return 0;
}
这样可以看出2000页的部分范围的地址。
若我们想知道某个内存块在哪个span,所以我们可以通过内存块的地址算出是哪个页,然后知道是哪个span,但因为要遍历每个span,所以时间复杂度较高。我们可以通过建立映射来解决
PageCache.cpp
//......
Span* PageCache::MapObjectToSpan(void* obj)//给了一个obj(地址)算span
{
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* span)
{
}
PageCache.h
添加
Span* MapObjectToSpan(void* obj);//获取从对象到span的映射
//
void ReleaseSpanToPageCache(Span* span); //释放空闲span回到Pagecache,并合并相邻的span
Span* NewSpan(size_t k);//获取一个k页的span
CentralCache.cpp
实现还给span
//......
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();
while (start)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//算出是哪个span
NextObj(start) = span->_freeList;//把对象头插到span
span->_freeList = start;
span->_useCount--;
// 说明span的切分出去的所有小块内存都回来了 ,这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
span->_freeList = nullptr;//清理
span->_next = nullptr;
span->_prev = nullptr;
// 释放span给page cache时,使用page cache的锁就可以了
// 这时把桶锁解掉,如果不解,其它线程想在这个桶申请释放内存操作不了
_spanLists[index]._mtx.unlock();//这里要解锁不然会影响其它对象返还内存
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
start = next;
}
_spanLists[index]._mtx.unlock();
}
CentralCache.h
添加
//将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
PageCache回收内存
当中心缓存span中usecount为0时,则释放回PageCache,再进行页合并,减小内存碎片。这一部分主要说明 ReleaseListToSpans。那么如何拼接内存碎片呢,若还回来的都是一二page的,如何正确拼接成一个大页使其连续可以使用呢?若我的pageId是666为两页,我们若1往前看,看665(若为三页)是否空闲,若空闲,则可以合成五页的span。往后看同理。若合成的页大于128就要停止,因为不知道挂在哪里
PageCache.cpp
#include "PageCache.h"
PageCache PageCache::_sInst;
// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
// 大于128 page的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;
Span* span = _spanPool.New();
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
_idSpanMap[span->_pageId] = span;
return span;
}
// 先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
Span* kSpan = _spanLists[k].PopFront();
// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
for (size_t i = k + 1; i < NPAGES; ++i)
{
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();
// 在nSpan的头部切一个k页下来
// k页span返回
// nSpan再挂到对应映射的位置
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
_spanLists[nSpan->_n].PushFront(nSpan);
// 存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时
// 进行的合并查找
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
}
// 走到这个位置就说明后面没有大页的span了
// 这时就去找堆要一个128页的span
//Span* bigSpan = new Span;
Span* bigSpan = _spanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 对span前后的页,尝试进行合并,缓解内存碎片问题
while (1)//因为有条件判断所以一定会退出
{
PAGE_ID prevId = span->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
// 前面的页号没有,不合并了
if (ret == _idSpanMap.end())
{
break;
}
// 前面相邻页的span在使用,不合并了
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)//这里添加了_isUse判断是否在进行使用
{
break;
}
// 合并出超过128页的span没办法管理也不合并
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
_spanLists[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
// 向后合并
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end())
{
break;
}
Span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_n += nextSpan->_n;
_spanLists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
_spanLists[span->_n].PushFront(span);
span->_isUse = false;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}
大于256kb的大块内存问题
当小于256kb时直接找三层缓存,当大于256kb时分两种情况,当32(页)*8k<size<=128(页)*8k时找PageCache,当大于128(页)*8k时找系统(堆)。
Common.h
重新修改一下计算大于256kb的情况
// 对齐大小计算
static inline size_t RoundUp(size_t bytes)
{
if (bytes <= 128) {
return _RoundUp(bytes, 8);
}
else if (bytes <= 1024) {
return _RoundUp(bytes, 16);
}
else if (bytes <= 8 * 1024) {
return _RoundUp(bytes, 128);
}
else if (bytes <= 64 * 1024) {
return _RoundUp(bytes, 1024);
}
else if (bytes <= 256 * 1024) {
return _RoundUp(bytes, 8 * 1024);
}
else
{
return _RoundUp(bytes, 1<<PAGE_SHIFT);
}
}
return _RoundUp(bytes, 1<<PAGE_SHIFT);
按8kb对齐 。
ConcurrentAlloc.h
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
size_t alignSize = SizeClass::RoundUp(size);
size_t kpage = alignSize >> PAGE_SHIFT;
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);//要的是地址
PageCache::GetInstance()->_pageMtx.unlock();
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
return ptr;
}
else
{
//每个线程获取自己的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
cout << std::this_thread::get_id() << ":" << pTLSThreadCache << std::endl;
return pTLSThreadCache->Allocate(size);
}
}
PageCache.cpp
//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
// 大于128 page的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
Span* span = new Span;
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
_idSpanMap[span->_pageId] = span;
return span;
}
if (!_spanLists[k].Empty())//先检查第k个桶有没有span
{
return _spanLists->PopFront();//拿出来
}
//检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
for (size_t i = k + 1; i < NPAGES; ++i)//从下一页开始找
{
if (!_spanLists[i].Empty())
{
//开始切
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
//处理剩下页数的span
_spanLists[nSpan->_n].PushFront(nSpan);
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
}
//走到这个位置说明后面没有大页的span了,这时向系统(堆)要一个128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n =NPAGES-1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
接下来我们要进行释放操作,虽然我们只知道释放对象的起始地址,但我们已经实现了其的映射关系,从而找到span。
PageCache.cpp
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 大于128 page的直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);//借助页号找到指针
SystemFree(ptr);
delete span;
return;
}
// ......
}
Common.h
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// sbrk unmmap等
#endif
}
ConcurrentAlloc.h
static void* ConcurrentFree(void* ptr, size_t size)
{
if (size > MAX_BYTES)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
CentralCache.cpp
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();
while (start)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
NextObj(start) = span->_freeList;
span->_freeList = start;
span->_useCount--;
// 说明span的切分出去的所有小块内存都回来了 ,这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
span->_freeList = nullptr;//清理
span->_next = nullptr;
span->_prev = nullptr;
// 释放span给page cache时,使用page cache的锁就可以了
// 这时把桶锁解掉,如果不解,其它线程想在这个桶申请释放内存操作不了
_spanLists[index]._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
start = next;
}
_spanLists[index]._mtx.unlock();
}
使用定长内存池配合脱离使用new
我们使用tcmalloc的就是在某些情况下相对于使用malloc更加的高效,实现替代malloc。那我们在自己实现的tcmalloc中更不能直接调用malloc/new与delete。所以我们要把代码中的malloc/new改为自己实现的代码,delete也改成自己写的。
PageCache.h
添加
ObjectPool<Span> _spanPool;
#pragma once
#include "Common.h"
#include "ObjectPool.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
// 获取一个K页的span
Span* NewSpan(size_t k);
std::mutex _pageMtx;
private:
SpanList _spanLists[NPAGES];
ObjectPool<Span> _spanPool;
//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
std::map<PAGE_ID, Span*> _idSpanMap;
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
再把new/malloc/delete全部换成自己实现的
可以看出来,仍然可以正常运行。
释放对象时优化为不传对象大小的
我们目前实现的回收内存都是要指明大小的,因为要区分是大块内存还是小块内存当小于256kb时直接找三层缓存,当大于256kb时分两种情况,当32(页)*8k<size<=128(页)*8k时找PageCache,当大于128(页)*8k时找系统(堆)。。如果我们不想传大小该怎么操作呢?我们可以用map实现一个映射实现简化。
Common.h
添加
size_t _objSize = 0; // 切好的小对象的大小
struct Span
{
PAGE_ID _pageId = 0; // 大块内存起始页的页号
size_t _n = 0; // 页的数量
Span* _next = nullptr; // 双向链表的结构
Span* _prev = nullptr;
size_t _objSize = 0; // 切好的小对象的大小
size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数
void* _freeList = nullptr; // 切好的小块内存的自由链表
bool _isUse = false; // 是否在被使用
};
CentralCache.cpp
span->_objSize = size;
把这个大小保存起来。
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 查看当前的spanlist中是否有还有未分配对象的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
list._mtx.unlock();
// 走到这里说没有空闲span了,只能找page cache要
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
span->_isUse = true;
span->_objSize = size;
PageCache::GetInstance()->_pageMtx.unlock();
// 对获取span进行切分,不需要加锁,因为这会其他线程访问不到这个span
// 计算span的大块内存的起始地址和大块内存的大小(字节数)
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t bytes = span->_n << PAGE_SHIFT;
char* end = start + bytes;
// 把大块内存切成自由链表链接起来
// 1、先切一块下来去做头,方便尾插
span->_freeList = start;
start += size;
void* tail = span->_freeList;
int i = 1;
while (start < end)
{
++i;
NextObj(tail) = start;
tail = NextObj(tail); // tail = start;
start += size;
}
NextObj(tail) = nullptr;
// 切好span以后,需要把span挂到桶里面去的时候,再加锁
list._mtx.lock();
list.PushFront(span);
return span;
}
ConcurrentAlloc.h
static void ConcurrentFree(void* ptr)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
if (size > MAX_BYTES)
{
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
PageCache.cpp
//......
Span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
std::unique_lock<std::mutex> lock(_pageMtx);
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
//......
多线程环境下对比malloc测试
Benchmark.cpp
ntimes 一轮申请和释放内存的次数,nworks创建多少线程,rounds 轮次
#include"ConcurrentAlloc.h"
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&, k]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
v.push_back(malloc(16));
//v.push_back(malloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
free(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n", (size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n", (size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)free_costtime);
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n", (size_t)nworks, (size_t)nworks * rounds * ntimes, (size_t)(malloc_costtime + free_costtime));
}
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
v.push_back(ConcurrentAlloc(16));
//v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
ConcurrentFree(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
(size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
(size_t)nworks, (size_t)rounds, (size_t)ntimes, (size_t)free_costtime);
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
(size_t)nworks, (size_t)nworks * rounds * ntimes, (size_t)(malloc_costtime + free_costtime));
}
int main()
{
size_t n = 1000;
cout << "==========================================================" << std::endl;
BenchmarkConcurrentMalloc(n, 4, 10);
cout << std::endl << std::endl;
//BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << std::endl;
return 0;
}
PageCache.cpp
#include "PageCache.h"
PageCache PageCache::_sInst;
// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
// 大于128 page的直接向堆申请
if (k > NPAGES-1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;
Span* span = _spanPool.New();
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
_idSpanMap[span->_pageId] = span;
return span;
}
if (!_spanLists[k].Empty())// 先检查第k个桶里面有没有span
{
Span* kSpan = _spanLists[k].PopFront();
// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
// 检查后面的桶里面有没有span,往大页找,例如在三页的位置有span则拿来用,先切分成两页的span和一页的span,两页的拿去给中心缓存用,剩下的一页挂在一页的位置上。
for (size_t i = k+1; i < NPAGES; ++i)
{
//开始切
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();
//在nSpan的头部切一个k页下来,k页返回,nSpan挂到对应映射位置
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
//处理剩下页数的span
_spanLists[nSpan->_n].PushFront(nSpan);
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
}
// 走到这个位置就说明后面没有大页的span了这时就去找堆要一个128页的span
//Span* bigSpan = new Span;
Span* bigSpan = _spanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
Span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
std::unique_lock<std::mutex> lock(_pageMtx);
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 大于128 page的直接还给堆
if (span->_n > NPAGES-1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
SystemFree(ptr);
//delete span;
_spanPool.Delete(span);
return;
}
// 对span前后的页,尝试进行合并,缓解内存碎片问题
while (1)
{
PAGE_ID prevId = span->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
// 前面的页号没有,不合并了
if (ret == _idSpanMap.end())
{
break;
}
// 前面相邻页的span在使用,不合并了
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}
// 合并出超过128页的span没办法管理,不合并了
if (prevSpan->_n + span->_n > NPAGES-1)
{
break;
}
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
_spanLists[prevSpan->_n].Erase(prevSpan);
//delete prevSpan;
_spanPool.Delete(prevSpan);
}
// 向后合并
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end())
{
break;
}
Span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}
if (nextSpan->_n + span->_n > NPAGES-1)
{
break;
}
span->_n += nextSpan->_n;
_spanLists[nextSpan->_n].Erase(nextSpan);
//delete nextSpan;
_spanPool.Delete(nextSpan);
}
_spanLists[span->_n].PushFront(span);
span->_isUse = false;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId+span->_n-1] = span;
}
可以看出来效率还是低于malloc,free。我们可以用VS2022自带的性能分析工具来分析是什么地方消耗时间较长。
我们可以看出来光ThreadCache中Deallocate函数和PageCache中MapObjectToSpan函数加起来就占了一大半时间。
在 ThreadCache中Deallocate函数
可以看出来ListTooLong占用时间最多
可以看出来ReleaseListToSpans占用时间最多
可以看出来在锁和MapObjectToSpan的调用最多
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();
while (start)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
NextObj(start) = span->_freeList;
span->_freeList = start;
span->_useCount--;
// 说明span的切分出去的所有小块内存都回来了
// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
// 释放span给page cache时,使用page cache的锁就可以了
// 这时把桶锁解掉
_spanLists[index]._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
start = next;
}
_spanLists[index]._mtx.unlock();
}
可以看出来因为锁竞争导致过多时间消耗。
使用基数树进行优化
我们直接借鉴tcmalloc源码中的基数树.因为我们是在x86架构下,所以采取两层基数树,若换成x64架构则必须是三层基数树。
PageMap.h
#pragma once
#include"Common.h"
// Single-level array
template <int BITS>
class TCMalloc_PageMap1 {
private:
static const int LENGTH = 1 << BITS;
void** array_;
public:
typedef uintptr_t Number;
//explicit TCMalloc_PageMap1(void* (*allocator)(size_t)) {
explicit TCMalloc_PageMap1() {
//array_ = reinterpret_cast<void**>((*allocator)(sizeof(void*) << BITS));
size_t size = sizeof(void*) << BITS;
size_t alignSize = SizeClass::_RoundUp(size, 1 << PAGE_SHIFT);
array_ = (void**)SystemAlloc(alignSize >> PAGE_SHIFT);
memset(array_, 0, sizeof(void*) << BITS);
}
// Return the current value for KEY. Returns NULL if not yet set,
// or if k is out of range.
void* get(Number k) const {
if ((k >> BITS) > 0) {
return NULL;
}
return array_[k];
}
// REQUIRES "k" is in range "[0,2^BITS-1]".
// REQUIRES "k" has been ensured before.
//
// Sets the value 'v' for key 'k'.
void set(Number k, void* v) {
array_[k] = v;
}
};
// Two-level radix tree
template <int BITS>
class TCMalloc_PageMap2 {
private:
// Put 32 entries in the root and (2^BITS)/32 entries in each leaf.
static const int ROOT_BITS = 5;
static const int ROOT_LENGTH = 1 << ROOT_BITS;
static const int LEAF_BITS = BITS - ROOT_BITS;
static const int LEAF_LENGTH = 1 << LEAF_BITS;
// Leaf node
struct Leaf {
void* values[LEAF_LENGTH];
};
Leaf* root_[ROOT_LENGTH]; // Pointers to 32 child nodes
void* (*allocator_)(size_t); // Memory allocator
public:
typedef uintptr_t Number;
//explicit TCMalloc_PageMap2(void* (*allocator)(size_t)) {
explicit TCMalloc_PageMap2() {
//allocator_ = allocator;
memset(root_, 0, sizeof(root_));
PreallocateMoreMemory();
}
void* get(Number k) const {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
if ((k >> BITS) > 0 || root_[i1] == NULL) {
return NULL;
}
return root_[i1]->values[i2];
}
void set(Number k, void* v) {
const Number i1 = k >> LEAF_BITS;
const Number i2 = k & (LEAF_LENGTH - 1);
ASSERT(i1 < ROOT_LENGTH);
root_[i1]->values[i2] = v;
}
bool Ensure(Number start, size_t n) {
for (Number key = start; key <= start + n - 1;) {
const Number i1 = key >> LEAF_BITS;
// Check for overflow
if (i1 >= ROOT_LENGTH)
return false;
// Make 2nd level node if necessary
if (root_[i1] == NULL) {
//Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
//if (leaf == NULL) return false;
static ObjectPool<Leaf> leafPool;
Leaf* leaf = (Leaf*)leafPool.New();
memset(leaf, 0, sizeof(*leaf));
root_[i1] = leaf;
}
// Advance key past whatever is covered by this leaf node
key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
}
return true;
}
void PreallocateMoreMemory() {
// Allocate enough to keep track of all possible pages
Ensure(0, 1 << BITS);
}
};
// Three-level radix tree
template <int BITS>
class TCMalloc_PageMap3 {
private:
// How many bits should we consume at each interior level
static const int INTERIOR_BITS = (BITS + 2) / 3; // Round-up
static const int INTERIOR_LENGTH = 1 << INTERIOR_BITS;
// How many bits should we consume at leaf level
static const int LEAF_BITS = BITS - 2 * INTERIOR_BITS;
static const int LEAF_LENGTH = 1 << LEAF_BITS;
// Interior node
struct Node {
Node* ptrs[INTERIOR_LENGTH];
};
// Leaf node
struct Leaf {
void* values[LEAF_LENGTH];
};
Node* root_; // Root of radix tree
void* (*allocator_)(size_t); // Memory allocator
Node* NewNode() {
Node* result = reinterpret_cast<Node*>((*allocator_)(sizeof(Node)));
if (result != NULL) {
memset(result, 0, sizeof(*result));
}
return result;
}
public:
typedef uintptr_t Number;
explicit TCMalloc_PageMap3(void* (*allocator)(size_t)) {
allocator_ = allocator;
root_ = NewNode();
}
void* get(Number k) const {
const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
const Number i3 = k & (LEAF_LENGTH - 1);
if ((k >> BITS) > 0 ||
root_->ptrs[i1] == NULL || root_->ptrs[i1]->ptrs[i2] == NULL) {
return NULL;
}
return reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3];
}
void set(Number k, void* v) {
ASSERT(k >> BITS == 0);
const Number i1 = k >> (LEAF_BITS + INTERIOR_BITS);
const Number i2 = (k >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
const Number i3 = k & (LEAF_LENGTH - 1);
reinterpret_cast<Leaf*>(root_->ptrs[i1]->ptrs[i2])->values[i3] = v;
}
bool Ensure(Number start, size_t n) {
for (Number key = start; key <= start + n - 1;) {
const Number i1 = key >> (LEAF_BITS + INTERIOR_BITS);
const Number i2 = (key >> LEAF_BITS) & (INTERIOR_LENGTH - 1);
// Check for overflow
if (i1 >= INTERIOR_LENGTH || i2 >= INTERIOR_LENGTH)
return false;
// Make 2nd level node if necessary
if (root_->ptrs[i1] == NULL) {
Node* n = NewNode();
if (n == NULL) return false;
root_->ptrs[i1] = n;
}
// Make leaf node if necessary
if (root_->ptrs[i1]->ptrs[i2] == NULL) {
Leaf* leaf = reinterpret_cast<Leaf*>((*allocator_)(sizeof(Leaf)));
if (leaf == NULL) return false;
memset(leaf, 0, sizeof(*leaf));
root_->ptrs[i1]->ptrs[i2] = reinterpret_cast<Node*>(leaf);
}
// Advance key past whatever is covered by this leaf node
key = ((key >> LEAF_BITS) + 1) << LEAF_BITS;
}
return true;
}
void PreallocateMoreMemory() {
}
};
PageCache.h
#pragma once
#include "Common.h"
#include"ObjectPool.h"
#include"PageMap.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;//获取实例对象
}
Span* MapObjectToSpan(void* obj);//获取从对象到span的映射
//
void ReleaseSpanToPageCache(Span* span); //释放空闲span回到Pagecache,并合并相邻的span
Span* NewSpan(size_t k);//获取一个k页的span
std::mutex _pageMtx;
private:
SpanList _spanLists[NPAGES];//NPAGES=129
ObjectPool<Span> _spanPool;
//std::unordered_map<PAGE_ID, Span* > _idSpanMap;
//std::unordered_map<PAGE_ID,size_t > _idSizeMap;
TCMalloc_PageMap1<32-PAGE_SHIFT> _idSpanMap;
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};