central cache
- [1 central cache框架结构](#1 central cache框架结构)
- [2 span结构](#2 span结构)
-
- [2.1 _pageId](#2.1 _pageId)
- [2.2 _n](#2.2 _n)
- [2.3 _next和_prev](#2.3 _next和_prev)
- [2.4 _objSize](#2.4 _objSize)
- [3 spanList结构](#3 spanList结构)
-
- [3.1 spanList的桶锁](#3.1 spanList的桶锁)
- [3.2 central cache单例模式 - 饿汉模式](#3.2 central cache单例模式 - 饿汉模式)
- [4 慢开始反馈调节算法](#4 慢开始反馈调节算法)
-
- [4.1 NumMoveSize()函数](#4.1 NumMoveSize()函数)
- [4.2 _maxSize的作用](#4.2 _maxSize的作用)
- [4.3 具体过程](#4.3 具体过程)
- [5 thread cache从central cache申请内存的过程](#5 thread cache从central cache申请内存的过程)
-
- [5.1 thread cache中FetchFromCentralCache函数实现](#5.1 thread cache中FetchFromCentralCache函数实现)
- [5.2 central cache中GetOneSpan函数实现](#5.2 central cache中GetOneSpan函数实现)
- [5.3 central cache中FetchRangeObj函数实现](#5.3 central cache中FetchRangeObj函数实现)
- [6 总结](#6 总结)
当申请的内存小于
256KB
的时候直接去thread cache
中取,如果申请内存大于256KB
或者thread cache
中没有足够的内存时,就要去central cache
中拿
1 central cache框架结构
central cache
和 thread cache
拥有一样的哈希桶的结构,好处是在thread cache
要从central cache
中取内存时,可以直接取。
central cache
的结构如下:
central cache
的每一个桶中挂的是span
,这个span
是什么下面会讲解。而span
里面挂的才是切好的内存块freeList
.
每个
span
管理的都是一个以页为单位的大块内存,每个桶里面的若干span
是按照双链表的形式链接起来的,并且每个span
里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。
2 span结构
1.span是以页为单位的大内存块 2.span是双向链表 3.span里面挂着freeList作为成员
根据上面的内存,编写span的结构:
cpp
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; // 是否在被使用
};
2.1 _pageId
每个程序运行起来后都有自己的进程地址空间,在32位平台下,进程地址空间的大小是2^32^;而在64位平台下,进程地址空间的大小就是2^64^。
页的大小一般是4K或者8K,我们以8K为例。
在32位平台下,进程地址空间就可以被分成 2^32^ / 2^13^ = 2^19^个页;
在64位平台下,进程地址空间就可以被分成 2^64^ / 2^13^ = 2^51^个页。
页号本质与地址是一样的,它们都是一个编号,只不过地址是以一个字节为一个单位,而页是以多个字节为一个单位。
由于页号在64位平台下的取值范围是,因此我们不能简单的用一个无符号整型来存储页号,这时我们需要借助条件编译来解决这个问题。
cpp
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#else
//linux
#endif
需要注意的是,在32位下,_WIN32
有定义,_WIN64
没有定义;而在64位下,_WIN32
和_WIN64
都有定义。因此在条件编译时,我们应该先判断_WIN64
是否有定义,再判断_WIN32
是否有定义。
2.2 _n
span
管理以页为单位的大块内存,至于一个span
管理的到底是多少个页,这不确定,因此,需要用_n
来记录一下。
2.3 _next和_prev
就和下面的结构是一样的
cpp
struct TreeNode
{
public:
TreeNode* _prev;
TreeNode* _next;
};
每一个节点都有它的前驱节点和后继节点,方便链接起来成为一个链表。
2.4 _objSize
这个表示切好的小对象的大小
。
3 spanList结构
对于一个带头双向循环链表,主要的操作就是头插,头删,任意位置插入,任意位置删除。
这里就不作多的解释,都比较简单,直接列出代码:
cpp
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;
}
void PushFront(Span* span)
{
Insert(Begin(), span);
}
Span* PopFront()
{
Span* front = _head->_next;
Erase(front);
return front;
}
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
// prev newspan pos
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
// 1、条件断点
// 2、查看栈帧
/*if (pos == _head)
{
int x = 0;
}*/
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx; // 桶锁
};
3.1 spanList的桶锁
thread cache
是每个线程都有一份,所以不需要加锁。而central cache
是线程共有的,所以得加锁。在central cache
中有很多spanList
,也就是有很多桶。当不同thread cache
访问不同的桶时,不需要加桶锁,当不同的thread cache
访问相同的桶时,需要加桶锁。
3.2 central cache单例模式 - 饿汉模式
central cache在整个进程中只有一个,对于这种只能创建一个对象的类,我们可以将其设置为单例模式。
单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式又分为饿汉模式和懒汉模式,懒汉模式相对较复杂,我们这里使用饿汉模式就足够了。
cpp
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);
// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
private:
SpanList _spanLists[NFREELIST];
private:
//将构造函数、成员变量私有化
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
};
将CentralCache
的构造函数和拷贝构造函数设置为私有,在C++11中也可以在函数声明的后面加上=delete
进行修饰。
CentralCache
类当中还需要有一个CentralCache
类型的静态的成员变量,当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。
4 慢开始反馈调节算法
当thread cache
向central cache
申请内存时,central cache
应该给出多少个对象呢?这是一个值得思考的问题,如果central cache
给的太少,那么thread cache
在短时间内用完了又会来申请;但如果一次性给的太多了,可能thread cache
用不完也就浪费了。
鉴于此,我们这里采用了一个慢开始反馈调节算法。当thread cache
向central cache
申请内存时,如果申请的是较小的对象,那么可以多给一点,但如果申请的是较大的对象,就可以少给一点。
4.1 NumMoveSize()函数
通过下面这个函数,我们就可以根据所需申请的对象的大小计算出具体给出的对象个数,并且可以将给出的对象个数控制到2~512
个之间。也就是说,就算thread cache
要申请的对象再小,我最多一次性给出512
个对象;就算thread cache
要申请的对象再大,我至少一次性给出2
个对象。
cpp
//static const size_t MAX_BYTES = 256 * 1024;
// 一次thread cache从中心缓存获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
//size越大(申请的越大) --> num越小(获得的越少)
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
这里代码为什么要这么设计?
假设申请的内存很大,size = 256KB,一次给512个,总共就是256 * 512 KB.
假设申请的内存比较小,size = 256B,一次给2个,总共就是256 * 2B.
可以发现差距很大。
于是为了起到一个平衡的作用,让申请size很大的内存时,不给太多;申请size很小的内存时,不给太少。就对申请个数的上界和下界进行了限制。申请的内存如果特别大,那么不能申请低于2个。申请的内存如果特别小,那么不能申请超过512个。
申请的特别多的就少给点(一次性不能给太多,会造成浪费),申请的特别少的就多给点(也不能给太少,不然会频繁的到central cache中要)。
同时,还要在freeList
中增加一个成员变量_maxSize
,并提供一个接口获得_maxSize
.
4.2 _maxSize的作用
cpp
class FreeList
{
public:
size_t& MaxSize()
{
return _maxSize;
}
private:
void* _freeList = nullptr;
size_t _maxSize = 1; //这里是新添的_maxSize
size_t _size = 0;
};
_maxSize
的作用就是调节。_maxSize
表示最大能分配的FreeList
的个数。如果每次要申请的个数都是最大个数(_maxSize)
,那么_maxSize+1
。这样,随着申请的次数变多,_maxSize
变大,每次能申请到的个数也就变多了。这就是慢增长的过程。
4.3 具体过程
cpp
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// 慢开始反馈调节算法 -- 小对象给多一点,大对象给少一点
//达到的目的:
//1.最开始不会一次向central cache一次批量要太多,因为要太多用不完
//2.如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
//3.size越大,一次向central cache要的batchNum就越小
//4.size越小,一次向central cache要的batchNum就越大(慢慢增长的)
size_t batchNum;
if (_freeLists[index].MaxSize() < SizeClass::NumMoveSize(size))
batchNum = _freeLists[index].MaxSize();
else
batchNum = SizeClass::NumMoveSize(size);
//size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
//头文件windows.h中有一个宏也叫min,min是函数模板,所以直接用windows.h中的了。所以会报错
//慢增长
if (batchNum == _freeLists[index].MaxSize())
{
_freeLists[index].MaxSize() += 1;
}
//未完...
}
5 thread cache从central cache申请内存的过程
回顾之前thread cache申请内存的代码.
cpp
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size); //alignSzie 表示最终内存给的字节数
size_t index = SizeClass::Index(size); //index表示内存块在数组中的索引
if (!_freeLists[index].Empty())
{
return _freeLists[index].Pop();
}
else
{
//没有内存块,就去centrleCache获取对象
return FetchFromCentralCache(index, alignSize);
}
}
当thread cache
中没有足够的内存时,会去central cache
中获取对象。
5.1 thread cache中FetchFromCentralCache函数实现
根据慢开始反馈调节算法
最终得到central cache
会给thread cache
的freeList
的个数,(要根据central cache
的具体情况返回最终实际会给thread cache
的freeList
的个数。这个后面再详细讲解。)这时候就要根据得到的不同的freeList
的个数来进行判断了:
1.如果只得到1个freeList
,那么直接拿去用,返回freeList
的起始地址即可。
2.如果得到了很多的freeList
,那么要将得到的多的freeList
继续挂载在原来的thread cache
的哈希桶中。(这样下次thread cache
再申请内存时,不会直接到central cache
中去要,而是优先在thread cache
中拿)
根据这样的思路,编写代码:
cpp
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// 慢开始反馈调节算法 -- 小对象给多一点,大对象给少一点
//达到的目的:
//1.最开始不会一次向central cache一次批量要太多,因为要太多用不完
//2.如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
//3.size越大,一次向central cache要的batchNum就越小
//4.size越小,一次向central cache要的batchNum就越大(慢慢增长的)
size_t batchNum;
if (_freeLists[index].MaxSize() < SizeClass::NumMoveSize(size))
batchNum = _freeLists[index].MaxSize();
else
batchNum = SizeClass::NumMoveSize(size);
//size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
//头文件windows.h中有一个宏也叫min,min是函数模板,所以直接用windows.h中的了。所以会报错
void* start = nullptr;
void* end = nullptr;
//慢增长
if (batchNum == _freeLists[index].MaxSize())
{
_freeLists[index].MaxSize() += 1;
}
//actualNum表示实际给的freelist的个数,batchNum表示thread chache要的freelist的个数
//这里后续讲解
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum > 0);
//如果实际只获取到1个
if (actualNum == 1)
{
assert(start == end);
return start;
}
else
{
//如果获取到多个,就应该把获取到的自由链表的头节点返回去
_freeLists[index].PushRange(NextObj(start), end, actualNum-1);
return start;
}
}
如何将多的freeList挂载到thread cache中的哈希桶(_freeList)中?
下面是FreeList的结构,新增了PushRange函数。
cpp
class FreeList
{
public:
void PushRange(void* start, void* end, size_t n)
{
NextObj(end) = _freeList; //相当于得到的新freeList的end的next指向_freeList
_freeList = start; //_freeList作为新的头节点
_size += n;
}
private:
void* _freeList = nullptr;
size_t _maxSize = 1;
size_t _size = 0;
};
上面代码的绘图过程如下:
5.2 central cache中GetOneSpan函数实现
在central cache
中写一个函数GetOneSpan
获得一个非空的Span
.
函数头的设计:
cpp
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
传入一个SpanList
和size
。主要功能就是在SpanList
中找,遇到非空的Span
就直接返回。
cpp
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();
return span;
}
5.3 central cache中FetchRangeObj函数实现
这个函数的作用就是从central cache
中获取一定数量的自由链表给thread cache
.
主要流程是:根据计算出的index
从spanList
中找到spanList[index]
,在里面找到一个非空的span
,然后从非空的span
中取出freeList
,根据之前计算出的batchNum
的个数计算出这个非空的span
能取出多少个freeList
(也就是真实能取出多少个freeList
,用actualNum
表示)。如果
actualNum
小于batchNum
,就有多少取多少。
cpp
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
//这里调用GetOneSpan时是持有锁的,所以在GetOneSpan中有一个解锁的过程
Span* span = GetOneSpan(_spanLists[index], size);
assert(span);
assert(span->_freeList);
// 从span中获取batchNum个对象
// 如果不够batchNum个,有多少拿多少
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;
条件断点
int j = 0;
void* cur = start;
while (cur)
{
cur = NextObj(cur);
++j;
}
if (j != actualNum)
{
int x = 0;
}
//解锁
_spanLists[index]._mtx.unlock();
return actualNum;
}
6 总结
- 讲解了central cache的结构
- 讲了spanList和span的数据结构,和一些接口
- 讲解了thread cache从central cache中申请内存的过程