Central Cache框架设计

当申请对象时,thread cache层对应的自由链表为空时,需要向central cache层申请内存。central cache层框架图如下:

9d6481a02fe24f458b37909674fc3d66.png

central cache整体也是一个哈希桶结构,类似于thread cache层,但是central cache层所挂的是span的链表

  • span是管理大块内存的数据结构,页为单位
  • span里面有个自由链表,管理切割出来的小内存块
  • 映射关系和对齐规则同thread cache

Central Cache具体结构

span结构

span是管理多个连续页大块内存的跨度结构,具体结构代码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14

struct Span
{
PAGE_ID _pageId = 0; //大块内存起始页的页号
size_t _n = 0; //页的数量

Span* _next = nullptr; //双向链表的结构
Span* _prev = nullptr;

size_t _useCount = 0; //切好的小块内存,被分配给thread cache的计数

void* _freeList =nullptr; //切好小块内存的自由链表
};

首先需要注意的是,span是双向链表的结构,双向链表删除元素非常方便,当将某个span对象归还给page cache层时,双向链表删除非常简单

span是管理以页为单位的大块内存,当归还给page cache层时,为了方便相邻页的合并需要一个页号 pageID,我们用了typedef的类型PAGE_ID

假设一页为8k
32位下 进程地址空间有4G,能分 2^32/2^13 = 2^19 个页
64位下 进程地址空间有16G,能分 2^64/2^13 = 2^51 个页

所以不同平台下,PAGE_ID 的类型需要动态调整,这里我们用条件编译实现

1
2
3
4
5
6
7
//x64  _WIN32和_WIN64都有定义
//win32 _WIN32有定义,_WIN64没有定义
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#endif // _WIN64

这里注意先判断 _WIN64

SpanList结构

我们知道central cache层每个哈希桶中挂的是span的链表,我们将这个链表结构定义成带头双向的循环链表

我们目前的核心成员有两个

  • Span*
  • mutex锁(桶锁,多个线程同时申请一个桶的对象时需要加锁)

提供,插入和删除和删除的接口

  • void Insert(Span* pos, Span* newSpan)
  • void Eerase(Span *pos)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
newSpan->_next = pos;
pos->_prev = newSpan;

prev->_next = newSpan;
newSpan->_prev = prev;
}

void Eerase(Span *pos)
{
assert(pos);
assert(pos != _head);

Span* prev = pos->_prev;
Span* next = pos->_next;

next->_prev = prev;
prev->_next = next;
}
private:
Span* _head;
public:
std::mutex _mtx; //桶锁

};

注意,erase时,不需要delete,只需要从链表移除即可,会归还给下一层的page cache

Central Cache结构

首先Central Cache是哈希桶结构,然后我们期望,每个线程都有属于自己的Thread Cache(用TLS实现无锁),但是Central Cache在整个进程中只有一个,所以我们可以设计成单例模式

单例模式采用饿汉模式,构造函数私有,在静态区创建一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 单例模式
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
// 接口???
// ...
private:
static CentralCache _sInst;
private:
CentralCache(){} //构造函数私有化
CentralCache(const CentralCache& c) = delete; //禁用拷贝
private:
SpanList _spanLists[NFREELIST]; // SpanList数组(哈希桶)
};

Central Cache逻辑实现

thread cache层,申请对象,对应自由链表为空时,需要向central cache层申请内存,我们在thread cache部分提供了一个接口FetchFromCentralCache,我们先从这里开始,逐步扩展到整体的central cache的逻辑实现

1
2
3
4
5
//从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
}

thread cache向central申请内存时,最好多给一些,避免频繁申请,那么该给多少比较合适呢?这里提出一个慢开始反馈算法,我们核心想法如下:

  1. 最开始不会一次向central cache一次批量的要太多,要太多可能会用不完
  2. 如果不断有要size大小内存的的需求,batchNum也会不断的增长直到上限
  3. size越大,一次向central cache要的batchNum越小
  4. size越小,一次向central cache要的batchNum越大。

根据上述规则,我们在SizeClass类增加下述函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一次thread cache从中心节点获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
size_t num = MAX_BYTES / size;

//一次批量移动多少个对象的(慢启动)上限值为[2,512]
// 小对象一次批量上限高
// 大对象一次批量上限低
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}

我们就可以在FetchFromCentralCache函数中调用上面函数获取应该给central cache批量层要多少个对象的个数

1
size_t batchNum = SizeClass::NumMoveSize(size);

但是若size比较小,batchNum刚开始的时候还是比较大,怎么让小对象它开始的时候也没那么大,满足规则2呢?

我们可以这样处理,在FreeList结构中,增加一个成员 _ maxSize,并初始化为1,这样thread cache层每一个桶的自由链表都有它的maxSize。并可以写下边的逻辑。

当batchNum和当前所在自由链表的MaxSize相同时,说明上边MaxSize是较小的那个,我们让MaxSize加1,若不断申请size大小的对象,MaxSize就会不断增大,而【2,512】就可以做这个增加的上限
1
2
3
4
5
size_t batchNum = std::min(SizeClass::NumMoveSize(size),_freeLists[index].MaxSize());
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;
}

知道要多少个对象后就向central cache层要batchNum个对象,但是这里就会出现一些问题,central cache层可能不够batchNum个对象,我们看下边处理

首先是给CentralCache类提供两个接口。一个是获取非空的Span,另一个就是获取batchNum个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
// 先算size大小的对象,对应哪个桶,然后就去哪个桶去申请
size_t index = SizeClass::Index(size);
// 申请-加桶锁
_spanLists[index]._mtx.lock();

// 首先保证有一个非空的Span
Span* span = GetOneSpan(_spanLists[index], size);
assert(span);
assert(span->_freeList);

// 从span中获取batchNum个对象
// 如果不够batchNum,有几个拿几个,返回实际拿到的个数

start = span->_freeList;
end = start;
int actualNum = 1;
size_t i = 0;
while (i<batchNum && NextObj(end) != nullptr)
{
end = NextObj(end);
++i;
++actualNum;
}

// 拿走 actualNum个对象
span->_freeList = NextObj(end);
NextObj(end) = nullptr;

// 解锁
_spanLists[index]._mtx.unlock();

return actualNum;
}

完善完成上述内容后,我们回到最开始,thread cache向central cache申请内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// 慢开始反馈调节算法
// 先算要多少个size大小的对象
size_t batchNum = std::min(SizeClass::NumMoveSize(size),_freeLists[index].MaxSize());
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;
}


// 知道要多少个对象后就向central cache层要batchNum个对象
void* start;
void* end;

size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum >= 1);

if (actualNum == 1)
{
//只有一个
assert(start == end);
return start;
}
else
{
// 将NextObj(start)-end 头插进自由链表,返回start
_freeLists[index].PushRange(NextObj(start), end);
return start;
}
}