高并发内存池
1. 项目介绍
1.1 这个项目是什么
今天要学习的项目的原型是Google开源项目tcmalloc ,全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关函数(malloc、free)。
此项目可以说是顶尖C++高手写出来了的,我们学习他不是为了造更好的轮子,而是学习其思想,学习如何去思考问题,我将把tcmalloc最核心的框架简化后拿出来,模拟实现一个自己的高并发内存池。
1.2 这个项目的意义
相信大家都有一个疑惑,既然系统已经提供了malloc、free,为什么Google的大佬要额外实现一个tcmalloc,tcmalloc和malloc有什么区别
我们要知道,malloc是C标准库函数,应该要保证在各种情况下都能够实现内存管理,简单来说就是通用,而通用就意味着不能保证各种情况下都高效,二者不可兼得,实际是malloc中为了保证线程安全,加锁解锁消耗了非常多的性能。malloc在单线程下性能比较高,但是在多线程下性能就比较低了,这个时候tcmalloc孕育而生。
1.3 项目准备
这个项目会用到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识。
2. 什么是内存池
2.1 池化技术
所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率。
在计算机中,有很多使用“池”这种技术的地方,除了内存池,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求,线程又进入睡眠状态。
2.2 内存池
- 内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;
- 同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。
2.3 内存池主要解决的问题
- 内存池最主要解决的是效率问题
- 其次还有内存碎片问题
内存碎片分为:内碎片和外碎片
-
内碎片:你申请了5B的内存,但系统实际给你了8B内存,那么这多出来的3B就是内碎片,即实际分配的内存比所需内存多的那部分就是内碎片
-
外碎片:

即因为内存中不连续的小块内存,当需要大块内存时,小块内存因为不连续,导致无法分配出去
2.4 malloc
C/C++中我们要动态申请内存都是通过malloc去申请内存,但是我们要知道,实际我们不是直接去堆获取内存的,
而malloc就是一个内存池。malloc()相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用。当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如windows的vs系列用的微软自己写的一套,linux gcc用的glibc中的ptmalloc。
下面有几篇关于这块的文章,关于ptmalloc,有兴趣大家可以去看看他的实现细节。
一文了解,Linux内存管理,malloc、free 实现原理
malloc()背后的实现原理——内存池
malloc的底层实现(ptmalloc)
3. 高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题
- 性能问题。
- 多线程环境下,锁竞争问题。
- 内存碎片问题。
concurrent memory pool主要由以下3个部分构成:
thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈。page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片的问题。

4. 申请内存
为方便学习,我们先只考虑高并发内存池的申请内存逻辑
4.1 thread cache
thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。

核心:以申请的内存块大小作为索引,来寻找对应的桶
- 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
- 如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。
- 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表并返回一个对象。
在设计其时,需要考虑一些细节:
- 申请的最小内存应该为多大? – 8B,因为对于每个小块内存,都需要在其内部存储一个指向下一个小块内存的指针,在32位下指针大小为4B,在64位下指针大小为8B,为了保证兼容两种模式,所以最小内存应为8B
- 每个桶之间应该间隔多大内存? 这个可以自行设定,需要考虑的是如果间隔为8B,那么需要256KB/8B = 32K个桶,非常庞大的数字,所以设置一个合适的间隔也比较重要
- 当我们申请的内存并不是刚好等于桶中小块内存大小时,我们需要向上取整,例如我们申请5B的内存,实际应该匹配到8B的小块内存,多出来的3B是内碎片,为了使桶的个数不要太多,无法避免内碎片
//小块内存头4/8个字节存储指向下一个小块内存
//用于找到下一个小块内存
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
class FreeList {
public:
void PushFront(void* obj)
{
NextObj(obj) = _freeList;
_freeList = obj;
_size++;
}
void PushRangeFront(void* begin, void* end,size_t n)
{
NextObj(end) = _freeList;
_freeList = begin;
_size += n;
}
void* PopFront()
{
void* obj = _freeList;
_freeList = NextObj(_freeList);
_size--;
return obj;
}
void PopRangeFront(void*& begin,void*& end,size_t n)
{
assert(n <= _size);
begin = _freeList;
void* cur = _freeList;
for (size_t i = 1; i < n; i++)
{
cur = NextObj(cur);
}
end = cur;
NextObj(end) = nullptr;
_freeList = NextObj(cur);
_size -= n;
}
bool Empty()
{
return _size == 0;
}
size_t& MaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList = nullptr;
size_t _maxSize = 1; //向centralcache申请内存次数,用于慢启动获取小块内存数
size_t _size = 0; //链表节点个数
};
class ThreadCache {
public:
//向thread cache申请内存
void* Allocate(size_t size);
//当thread cache没有对应小块内存时,会从central cache中获取小块内存
void* FetchFromCentralCache(size_t index, size_t size);
-------------------------------------------------------------------------
//释放逻辑
void Deallocate(void* ptr,size_t size);
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[FREELISTS_NUM];
};
//thread local storage技术(TLS) 虽然声明是全局静态变量,但是能够保证每个进程只有一份
static __declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
我们需要确定桶的个数FREELISTS_NUM,桶内存块间隔
我采用的是分段确定:threadCache中每个桶所含的内存块大小不一
- [1B,128B] 中有16个桶,相邻桶之间内存大小相差8B
- [129B,1KB] 有56个桶,相邻桶之间内存大小相差16B
- [1KB+1B,8KB] 有56个桶,相邻桶之间内存大小相差128B
- [8KB+1B,64KB] 有56个桶,相邻桶之间内存大小相差1KB
- [64KB+1B,256KB] 有24个桶,相邻桶之间内存大小相差8KB
- 总计208个桶
这样的设计能保证每个小块内存的内碎片最大在百分之10左右
我们需要一个用于对齐的类:SizeClass
class SizeClass {
public:
//对所需内存字节数向上取整
static size_t _RoundUp(size_t size,size_t align)
{
return (size + align - 1) & ~(align - 1);
}
static 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 <= MAX_BYTES) return _RoundUp(size, 8*1024);
else {
//这里是超过MAX_BYTES的情况
//超过MAX_BYTES则直接按页对齐
return _RoundUp(size, 1 << PAGE_SHIFT);
}
}
static size_t _Index(size_t size,size_t align_shift)
{
return ((size + (1 << align_shift) - 1) >> align_shift) - 1;
}
//对所需字节数向上取整,并得到其桶下标
static size_t Index(size_t size)
{
assert(size <= MAX_BYTES);
/*
* threadCache中每个桶所含的内存块大小不一
* [1,128] 中有16个桶,相邻桶之间内存大小相差8B
* [129,1024] 有56个桶,相邻桶之间内存大小相差16B
* [1025,8*1024] 有56个桶,相邻桶之间内存大小相差128B
* [8*1024+1,64*1024] 有56个桶,相邻桶之间内存大小相差1024B
* [64*1024+1,256*1024] 有24个桶,相邻桶之间内存大小相差8*1024B
*/
static const int group_array[] = { 16,56,56,56,24 };
if (size <= 128) return _Index(size, 3);
else if (size <= 1024) {
return _Index(size - 128, 4) + group_array[0];
}
else if (size <= 8 * 1024) {
return _Index(size - 1024, 7) + group_array[1] + group_array[0];
}
else if (size <= 64 * 1024) {
return _Index(size - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
}
else if (size <= 256 * 1024) {
return _Index(size - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else assert(false);
return -1;
}
//threadcache一次向centralcache要的obj数量的上限
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
//慢启动策略
//小对象一次批量申请的上限高
//大对象一次批量申请的上限低
size_t num = MAX_BYTES / size;
if (num < 2) num = 2;
else if (num > 512) num = 512;
return num;
}
//central cache一次向page cache索要的span的页数
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t pages = num * size;
pages >>= PAGE_SHIFT;
if (pages < 1) pages = 1;
return pages;
}
}
向thread cache申请小块内存具体实现
//内存池最大申请可申请字节数
static const size_t MAX_BYTES = 256 * 1024;
//ThreadCache和CentralCache中桶的个数
static const size_t FREELISTS_NUM = 208;
void* ThreadCache::Allocate(size_t size)
{
//申请的内存必须合法
assert(size > 0 && size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size);
size_t index = SizeClass::Index(size);
//如果桶上有小块内存则返回给用户
if (!_freeLists[index].Empty())
{
return _freeLists[index].PopFront();
}
//链表中没有空闲内存需要向CentralCache申请
else
{
return FetchFromCentralCache(index, alignSize);
}
}
//从CentralCache中获取多个小块内存
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
/* 慢启动
* 每个桶有自己的maxSize,表示申请过多少次,每次成功申请都会增加其值
* 要申请的空闲内存块数 = min(maxSize,MAX_BYTES/size)
* MAX_BYTES/size:小对象申请的上限大,大对象申请的上限小
*/
size_t batch = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (batch == _freeLists[index].MaxSize()) _freeLists[index].MaxSize()++;
void* start = nullptr;
void* end = nullptr;
//FetchRangeObj是CentralCache的方法
size_t actual_batch = CentralCache::GetInstance()->FetchRangeObj(start,end, batch,size);
//如果actual_num为0,则表示内存获取失败,
// 1.要么申请内存没申请到,则在申请处会抛异常的,不会走到这
// 2.要么程序逻辑错误,应该断言检查
assert(actual_batch > 0);
if (actual_batch > 1)
{
_freeLists[index].PushRangeFront(NextObj(start), end, actual_batch - 1);
}
else
{
assert(start == end);
}
return start;
}
4.2 central cache
central cache也是一个哈希桶结构,他的哈希桶的映射关系跟thread cache是一样的。不同的是他的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。

这里重点要理解Span类,
其有如下字段:
//Span一个跨度的大块内存
// 管理以页为单位的大块内存
// 管理多个连续页大块内存跨度结构
struct Span {
//页号
PAGE_ID _pageId = 0;
//页数
size_t _n = 0;
//指向下一个Span
Span* _next = nullptr;
//指向前一个Span
Span* _prev = nullptr;
//指向小对象单链表的第一个节点,串起一堆小对象
void* _freeList = nullptr;
----------------------------------------------------------
//这三个字段,在释放逻辑会详细讲解
//所含每个小对象的大小
size_t _objSize = 0;
//已经被分配给thread cache的小对象个数
size_t _useCount = 0;
//这个Span是否从page cache上取下
bool _isUse = true;
};
- pageId:即页号,现在不需要太琢磨与它,待会在page cache处我会详细讲解,现在只需认为,它可以转变为向系统申请的内存的首地址
- n:即页数,表示当前Span管理着多少页内存
- 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对象的数量使用了类似网络tcp协议拥塞控制的慢开始算法;central cache也有一个哈希映射的spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不过这里使用的是一个桶锁,尽可能提高效率。
- central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的span对象,拿到span以后将span管理的内存按大小切好作为自由链表链接到一起。然后从span中取对象给thread cache。
- central cache的中挂的span中useCount记录分配了多少个对象出去,分配一个对象给threadcache,就++useCount
CentralCache全局只有一个,所以需要用到单例模式,这里采用饿汉模式
class CentralCache {
public:
//在对应的桶中获取一个含有小对象的Span
Span* GetOneSpan(size_t index,size_t size);
//获取一批小对象
size_t FetchRangeObj(void*& begin, void*& end, size_t batch, size_t size);
//释放逻辑
void ReleaseListToSpans(void* begin,size_t size);
static CentralCache* GetInstance()
{
return &_sInst;
}
private:
SpanList _spanLists[FREELISTS_NUM];
static CentralCache _sInst;
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
CentralCache& operator=(const CentralCache&) = delete;
};
CentralCache CentralCache::_sInst;
//获取一些小块内存
size_t CentralCache::FetchRangeObj(void*& begin, void*& end, size_t batch, size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index].Mutex()->lock();
Span* span = GetOneSpan(index,size);
assert(span);
assert(span->_freeList != nullptr);
//从span的小块内存中,选取一部分
void* cur = span->_freeList;
batch--;
size_t actualNum = 1;
begin = cur;
while (batch > 0 && NextObj(cur) != nullptr)
{
cur = NextObj(cur);
actualNum++;
batch--;
}
span->_freeList = NextObj(cur);
NextObj(cur) = nullptr;
end = cur;
span->_useCount += actualNum;
_spanLists[index].Mutex()->unlock();
return actualNum;
}
//从spanList上获取一个含有空闲小块内存的span
Span* CentralCache::GetOneSpan(size_t index,size_t size)
{
//首先找对应的SpanList,查看是否有Span含有小对象内存
Span* it = _spanLists[index].Begin();
while (it != _spanLists[index].End())
{
if (it->_freeList != nullptr)
{
return it;
}
it = it->_next;
}
_spanLists[index].Mutex()->unlock();
//走到这里说明没有空闲Span了,需要向PageCache要
PageCache::GetInstance()->Mutex()->lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
PageCache::GetInstance()->Mutex()->unlock();
//从PageCache获取到Span后,将Span所管理的大块内存,切成许多小块内存,挂在freeList上
//获取从PageCache得到的大块内存的首尾地址
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t bytes = span->_n << PAGE_SHIFT;
char* end = start + bytes;
//将大块内存切分为一块一块小内存,并链接到freeList上
span->_freeList = start;
start += size;
void* tail = span->_freeList;
while (start < end)
{
NextObj(tail) = start;
tail = NextObj(tail);
start += size;
}
NextObj(tail) = nullptr;
span->_objSize = size;
_spanLists[index].Mutex()->lock();
_spanLists[index].PushFront(span);
return span;
}
4.3 page cache
page cache也是一个哈希桶,桶的下标是根据Span所含页数确定的,1到128页就有128个桶,每个桶存储SpanList

- 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页pagespan分裂为一个4页page span和一个6页page span。
- 如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。
- 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
PageCache是系统和CentralCache之间的桥梁,负责向系统申请大块内存,然后将内存构建成Span,以便CentralCache使用
PageCache是按页的倍数向系统申请内存的,然后将得到的内存首地址除上一页的大小,就能得到页号,即pageId,而再加一个页数n,即可管理整个大块内存
PageCache结构:PageCache也采用单例模式
//PageCache的桶数
static const size_t KPAGE = 129;
//一个Page的大小:2^13 -- 可以改进一下,先获取系统一页的大小,然后再确定
static const size_t PAGE_SHIFT = 13;
class PageCache {
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//获取一个新的Span
Span* NewSpan(size_t kpages);
//根据地址索引到Span
Span* MapObjToSpan(void* obj);
//释放Span
void ReleaseSpan(Span* span);
std::mutex* Mutex()
{
return &_mtx;
}
private:
SpanList _pageLists[KPAGE];
//pageId索引得到Span
std::unordered_map<PAGE_ID, Span*> _idSpanMap;
std::mutex _mtx;
private:
PageCache()
{}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
};
Span* PageCache::NewSpan(size_t kpages)
{
assert(kpages >= 1);
//检查第一个桶有没有Span
if (!_pageLists[kpages].Empty())
{
Span* span = _pageLists[kpages].PopFront();
//将分出给CentralCache的span每个页与其指针映射起来
for (size_t i = 0; i < span->_n; i++)
{
_idSpanMap[span->_pageId + i] = span;
}
span->_isUse = true;
return span;
}
for (size_t i = kpages + 1; i < KPAGE; i++)
{
//如果比kpages大的桶中有span则直接分割span
if (!_pageLists[i].Empty())
{
Span* span = _pageLists[i].PopFront();
Span* newSpan = new Span;
newSpan->_pageId = span->_pageId;
newSpan->_n = kpages;
span->_n -= kpages;
span->_pageId += kpages;
_pageLists[span->_n].PushFront(span);
//如果是在PageCache中保存的页,只需要将其首尾页进行映射即可
_idSpanMap[span->_pageId]=span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
//将分出给CentralCache的span每个页与其指针映射起来
for (size_t i = 0; i < newSpan->_n; i++)
{
_idSpanMap[newSpan->_pageId + i] =newSpan;
}
return newSpan;
}
}
//如果比kpages大的桶中没有span,则向系统申请
//像系统申请的内存根据其地址,直接映射为页号
//只要保证PAGE和系统页面是一样大小就行
void* ptr = SystemAlloc(KPAGE-1);
Span* span = new Span;
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = KPAGE-1;
_pageLists[span->_n].PushFront(span);
return NewSpan(kpages);
}
5. 释放内存
5.1 thread cache
- 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
- 当链表的长度过长,则回收一部分内存对象到central cache。
//释放对象内存给ThreadCache
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(size > 0 && size <= MAX_BYTES);
size_t index = SizeClass::Index(size);
_freeLists[index].PushFront(ptr);
//如果现在ThreadCache中自由链表空闲内存过多就将其打包释放给CentralCache
//此实现细节只考虑了自由链表过长的情况,还可以加上自由链表所含闲置内存达到一个阈值时触发
if (_freeLists[index].MaxSize() <= _freeLists[index].Size())
{
ListTooLong(_freeLists[index], size);
}
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* begin;
void* end;
list.PopRangeFront(begin, end, list.MaxSize());
CentralCache::GetInstance()->ReleaseListToSpans(begin, size);
}
5.2 central cache
当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时–useCount。当useCount减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。
//将ThreadCache释放的小块内存重新挂到Span上,当此span小块内存都被归还时,再释放到PageCache中
void CentralCache::ReleaseListToSpans(void* begin,size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index].Mutex()->lock();
//将每块小内存通过映射找到其对应的span,并挂上去
while (begin != nullptr)
{
void* next = NextObj(begin);
Span* span = PageCache::GetInstance()->MapObjToSpan(begin);
//头插入span的freeList上
NextObj(begin) = span->_freeList;
span->_freeList = begin;
span->_useCount--;
begin = next;
//如果span的所有小块内存均为被使用则将其回收到PageCache中
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
_spanLists[index].Mutex()->unlock();
PageCache::GetInstance()->Mutex()->lock();
PageCache::GetInstance()->ReleaseSpan(span);
PageCache::GetInstance()->Mutex()->unlock();
_spanLists[index].Mutex()->lock();
}
}
_spanLists[index].Mutex()->unlock();
}
由于需要通过小块内存首地址索引到对应的Span,PageCache应该提供MapObjToSpan(void* obj)以供索引
//将对象地址转换为页号,寻找对应的span
Span* PageCache::MapObjToSpan(void* obj)
{
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
std::unique_lock<std::mutex> uniqueMtx(*PageCache::GetInstance()->Mutex());
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
if (span != nullptr)
return span;
//一定是能找到的,如果找不到则说明发生了错误
else
{
assert(false);
return nullptr;
}
}
5.3 page cache
如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
//将CentralCache传来的空闲span与页号相邻的span合并
void PageCache::ReleaseSpan(Span* span)
{
//先将小于此span第一页的相邻页合并
while (1)
{
PAGE_ID id = span->_pageId;
auto ret = _idSpanMap.find(id - 1);
//如果前一个span不存在,则退出
if (ret == _idSpanMap.end()) break;
Span* prevSpan = ret->second;
//如果前一个span还在被使用,则退出
//此处不能使用useCount,因为当span刚被申请出来,此时为0,但是不能将其合并
if (prevSpan->_isUse) break;
//如果与前一个span合并后超过了PageCache能挂的最大Span,则退出
if (prevSpan->_n + span->_n > KPAGE - 1) break;
_pageLists[prevSpan->_n].Erase(prevSpan);
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
delete prevSpan;
}
//将在此span页之后的相邻span合并
while (1)
{
PAGE_ID id = span->_pageId;
auto ret = _idSpanMap.find(id + span->_n);
//如果后一个span不存在,则退出
if (ret == _idSpanMap.end()) break;
Span* nextSpan = ret->second;
//如果前一个span还在被使用,则退出
//此处不能使用useCount,因为当span刚被申请出来,此时为0,但是不能将其合并
if (nextSpan->_isUse) break;
//如果与后一个span合并后超过了PageCache能挂的最大Span,则退出
if (nextSpan->_n + span->_n > KPAGE - 1) break;
_pageLists[nextSpan->_n].Erase(nextSpan);
span->_n += nextSpan->_n;
delete nextSpan;
}
//合并后要修改pageId和span的索引,不然会找到之前被释放的span
//_idSpanMap[span->_pageId] = span;
//_idSpanMap[span->_pageId + span->_n - 1] = span;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
span->_isUse = false;
_pageLists[span->_n].PushFront(span);
}
6. 对象池取代new、delete
template<class T>
class ObjectPool {
static const size_t MAX_BYTES = 128*1024;
inline static void* SystemAlloc(size_t size)
{
assert(size > 0);
#ifdef _WIN32
void* ptr = VirtualAlloc(0, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
//linux
#endif
if (ptr == nullptr) throw std::bad_alloc();
return ptr;
}
inline static void*& NextObj(void* obj)
{
return *(void**)obj;
}
public:
T* New()
{
T* obj = nullptr;
_mtx.lock();
if (_freeList != nullptr)
{
obj = (T*)_freeList;
_freeList = NextObj(_freeList);
}
else
{
if (_remainSize < sizeof(T))
{
size_t allocSize = max(sizeof(T), MAX_BYTES);
_mem = (char*)SystemAlloc(allocSize);
_remainSize = allocSize;
}
size_t size = max(sizeof(T), sizeof(void*));
_remainSize -= size;
obj = (T*)_mem;
_mem += size;
}
_mtx.unlock();
new(obj)T;
return obj;
}
void Delete(T* obj)
{
obj->~T();
_mtx.lock();
NextObj(obj) = _freeList;
_freeList = obj;
_mtx.unlock();
}
private:
char* _mem = nullptr;
size_t _remainSize = 0;
void* _freeList = nullptr;
std::mutex _mtx;
};
利用了池化技术,先申请一大块内存自己管理
于是可以将项目中的new与delete替换为对象池的New和Delete
7. 用户接口
要为用户封装一个接口以调用申请和释放函数
static void* ConcurrentAllocate(size_t size)
{
if (pTLSThreadCache == nullptr)
{
static ObjectPool<ThreadCache> threadPool;
pTLSThreadCache = threadPool.New();
}
assert(pTLSThreadCache);
if (size > MAX_BYTES)
{
size_t alignSize = SizeClass::RoundUp(size);
size_t kpage = alignSize >> PAGE_SHIFT;
PageCache::GetInstance()->Mutex()->lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);
PageCache::GetInstance()->Mutex()->unlock();
span->_objSize = alignSize;
return (void*)(span->_pageId << PAGE_SHIFT);
}
else
{
return pTLSThreadCache->Allocate(size);
}
}
static void ConcurrentFree(void* ptr)
{
assert(pTLSThreadCache);
PAGE_ID id = (PAGE_ID)ptr >> PAGE_SHIFT;
Span* span = PageCache::GetInstance()->MapObjToSpan(ptr);
size_t size = span->_objSize;
if (size > MAX_BYTES)
{
PageCache::GetInstance()->Mutex()->lock();
PageCache::GetInstance()->ReleaseSpan(span);
PageCache::GetInstance()->Mutex()->unlock();
}
else pTLSThreadCache->Deallocate(ptr,size);
}
8. 优化项目性能
通过测试会发现,MapObjToSpan(void* obj)占用了大量性能,这是因为unordered_map在增加元素时,可能导致结构变化,所以在使用时需要加锁,但是其调用较为频繁,大量的申请和释放锁,肯定会让性能大打折扣
我们可以利用radix基数树替换unordered_map,以达到访问设置索引时无需加锁。
radix是在trie的基础上,优化了空间而产生的
可以通过具体代码来理解此结构
// 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() {
}
};
大佬们在实现了三种结构:
- 第一种为直接哈希
- 第二种为两层结构,例:假设32位下,一页大小为8K,那么页号只有32-13 = 19位,通过页号找到Span的方式如下

- 第三种和第二种类似
在使用时32位三种结构都可以用,但是64位只能用第三种
由于radix在插入时不会更改结构,且查询时有central cache的桶锁在,不会有线程刚好更改Span所在位置,所以无需加锁,大大提高了性能
9. tcmalloc替换malloc
我们能否替换到系统调用malloc呢?
不同平台替换方式不同。 基于unix的系统上的glibc,使用了weak alias的方式替换。具体来说是因为这些入口函数都被定义成了weak symbols,再加上gcc支持 alias attribute,所以替换就变成了这种通用形式
void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc)))
因此所有malloc的调用都跳转到了tc_malloc的实现
具体可以参考GCC attribute 之weak,alias属性
有些平台不支持这样的东西,需要使用hook的钩子技术来做.
关于hook请看这里:hook
773

被折叠的 条评论
为什么被折叠?



