本项目仅为了学习并提升代码能力,不作为实际运用。
项目完整代码地址:gitee仓库地址
项目原型是google
的开源项目tcmalloc
。即线程缓存的malloc
,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数malloc
和free
。
项目特点:1.比较难 2.知名度高(很多的大厂程序员都知道这个项目,并且go
语言的内存分配器就是这个)所以面试官可能会问的很细。
知识点:C/C++
,数据结构(链表,哈希桶),操作系统内存管理,单例模式,多线程,互斥锁
“池化技术”就是程序向系统先申请过量资源,然后自己管理,以备不是之需。因为每一次申请资源都需要较大的开销,所以提前申请好了资源,这样在使用的时候,就会大大提高程序运行的效率。
除了内存池,还有连接池,线程池,对象池等等。以线程池为例,它的主要思想就是:先启动若干数量的线程,让它们先处于睡眠状态,当接收客户端的请求的时候,唤醒线程池中的某个睡眠的线程来处于客户端的请求,当处理完这个请求后,该线程再进入睡眠状态。
原理和线程池类似
内存池主要可以解决两个方面的问题:
内存碎片分两种
malloc
实际就是一个内存池,malloc
相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给不同的进程。
但是malloc
的实现方式有很多种。windows
中有自己vs
系列的一套,linux
中有ptmalloc
。
有很多的文章讲到了malloc
的实现方式可以看看malloc
的实现。
tcmalloc
比普通的malloc
要快,并且在多线程高发可以很快
先做一个定长的内存池,一方面可以先熟悉一下内存池,另一方面可以作为后面项目的一个基础组件。
定长内存池的功能:
项目特点:
设计思想:
问题1:不采用
void* _memory
,而是采用char* _memory
?
使用char*方便后面可以方便切内存使用
问题2:如何处理归归还之后的内存呢?
采用自由链表的方式。使用void* freeList
存储第一个归还的内存块的地址,然后第二个内存块(32位下必须大于4字节)的头4字节去存储第二个内存块的地址,最后一个在freeList
中的内存块指向nullptr
即可。也就是freeList
中的节点就是一个个归还回来的内存块 。
问题3:如何知道何时再分配资源?
引入成员变量_leftBytes
,统计当前内存池还剩下可用的内存块的字节数。当剩余空间小于一个申请对象的时候,这个时候就说明当前的内存块不够用了,所以就需要重新的申请空间。而当前剩下的一些内存就不要了。
问题4:如何将第一个内存块放入一个空的
_freeList
中?
我们只需要将_freeList
指向第一块内存即可。但是又有一个问题,此时这个内存块既是第一个内存块也是最后一个内存块,所以需要将内存块的前部分指向nullptr
,而空指针是4个字节,因为需要将头4个字节填上nullptr
。这里有一个技巧取用头4个字节:可以先将obj强转成(int*)
,然后再解引用就可以拿到4个字节了。(使用不同类型的指针访问内存是一个技巧)。
问题5:32位上程序是没有问题的,但是64位上程序就不对了。
因为32位下的指针大小是4字节,64位下的指针是8字节,那么为了保证开辟的空间大小正确,可以开辟一个指针的大小同时也可以转成一个指针,所以就可以强转成(void**)
(解释:将void*
看成一个整体,那么我们就需要开辟一个void*
大小的空间,此时指针void*
就可以随着平台的不同而产生变化也就可以满足我们的需求),然后再解引用,即*(void**)obj = nullptr
。当然如果麻烦一点的话,就可以直接判断当前平台下一个指针的大小,根据一个指针的大小使用if
判断开多大的空间。
问题6:删除操作的简便写法。
我们在处理归还的费第一个节点时,采用头插法的效率最高。那么其实就可以直接所有的插入操作都写成头插,这样也不用特殊处理第一个节点了。
问题7:在分配空间的最开始,应该考虑
_freeList
是否有可用的空间
在分配空间的时候,需要先考虑回收的自由链表中是否存在可用的内存。如果自由链表中存在可用的内存,那么就不用向系统再申请内存空间了。
问题8:如果归还的内存块不满4/8个字节也就是不能存放一个指针的大小,也就无法保存下一个空间的地址,怎么办?
为了让一个内存块一定可以保存一个指针,所以在分配内存空间的时候,我们需要判断一个内存块的大小,如果大于一个指针的大小,那么可以直接分配;如果小于一个指针的大小,就可以分配一个指针的大小。这样就可以保证每一个内存块都一定可以保存一个指针的大小。
问题9:需要主动处理内存块中对象的内容
当分配空间的时候,需要使用定位new去主动的调用T
对象的构造函数。在将内存块回收的时候,需要主动调用T
对象的析构函数。
operator new
分配好内存空间后,可以使用定位去驱主动的调用构造函数问题10:如果我们想要使得我们制作的内存池更加纯粹的话,那么申请空间的时候就不使用
malloc
而是直接向系统申请内存。
malloc
是一个内存池,所以为了使得申请内存资源的操作更加纯粹的话,可以直接使用相关的系统接口,以页为单位向系统直接申请系统内存。
如果想要直接向系统申请内存的话,在windows
下可以使用VirtualAlloc
,在linux
下可以使用brk()
或者mmap()
。
mmap
可以将文件的内容映射进进程的虚拟地址空间,这样就可以不用read
和write
对文件进行操作。brk
是将数据段的最高地址指针_edata
指针往高地址推#include
int brk(void* addr);
brk
指针指向addr
的位置上addr
:将brk
推到addr
的位置上#include
void* sbrk(intptr_t increment);
brk
指针,增加increment
大小的内存increment
:增加的内存大小brk
指向的位置**使用技巧:**使用sbrk
可以更方便地分配指定的内存空间,因为在释放空间的时候必须要重新定位指针的位置。使用brk
可以更方便地释放内存,因为不能确定brk
指针的位置。
所以设置一个brk
指针的锚点,使用sbrk
动态分配内存,而brk
可以以锚点为基础回收内存。
#pragma once
#include
using std::cout;
using std::endl;
#ifdef _WIN32
// 因为是在vs下变成,所以使用windows系统分配内存的接口
#include
#else
// 如果是Linux就要使用Linux下直接分配内存的接口
#include
#endif
// 按页分配,一页是8k
// (1 << 13)就是8*1024
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage*(1 << 13), MEM_COMMIT | MEM_RESERVE, PAGE_READONLY);
#else
// linux下mmap接口
void* ptr = mmap(0,//首地址,0代表内核指定
kpage * (1 << 13), // 开辟K页内存
PROT_READ|PROT_WRITE,//权限
MAP_PRIVATE|MAP_ANONYMOUS,//私有匿名 针对
0,0);//文件描述符
#endif
}
// 定长内存池
// 非类型模板参数直接确定内存池的大小
//template
//class ObjectPool
//{};
// 但是为了后面的项目准备,所以这里写成class T,而T对象的大小也是固定的,也是可以当做一个常数使用的
template<class T>
class ObjectPool
{
public:
T* New()
{
T* obj = nullptr;
// 问题7
if (_freeList != nullptr)
{
void* next = *(void**)_freeList;
obj = (T*)_freeList;
_freeList = next;
}
else
{
// 问题3
// 剩余内存不够一个对象大小是,重新开空间
if (_leftBytes < sizeof(T))
{
// 问题10
_leftBytes = 128 * 1024;
// _memory = (char*)malloc(_leftBytes);
_memory = (char*)SystemAlloc(_leftBytes >> 13); // 16页
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
// 问题8
// _memory += sizeof(T);
// _leftBytes -= sizeof(T);
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_leftBytes -= objSize;
}
// 问题9
new(obj)T;
return obj;
}
void Delete(T* obj)
{
/**
// 问题4
if (nullptr == _freeList)
{
_freeList = obj;
// 问题5
// *(int*)obj = nullptr;
*(void**)obj = nullptr;
}
else // 头插
{
*(void**)obj = _freeList;
_freeList = obj;
}
*/
// 问题9
obj->~T();
// 问题6 && 问题8
*(void**)obj = _freeList;
_freeList = obj;
}
private:
// 可以直接给缺省值,就不用写构造函数了
// 指向大块内存的指针
char* _memory = nullptr; // 问题1
// 大块内存中剩余字节数
size_t _leftBytes = 0;
// 还回来的内存形成的单链表
void* _freeList = nullptr; // 问题2
};
malloc
本身已经很优秀了,但是本项目中tcmalloc
在多线程高并发的场景下更胜一筹,所以实现的内存池需要考虑一下问题:
ConcurrentMemoryPool
主要有以下的3个部分组成:
thread cache
:线程缓存是每个线程独有(后面会讲实现) 的,用于小于256KB的内存分配,线程从这个申请内存是不需要加锁的,每一个线程独享一个cache
,这就是并发线程池高效的问题。central cache
:中心缓存是所有线程共享的。thread cache
按需从central cache
中获取对象的。central cache
在适合的时机(后面会讲实现) 回收thread cache
中的对象,避免一个线程会占用太多的资源,而其他的线程会资源紧缺,达到了内存分配在多个线程中更均衡的按需调度的目的。 central cache
在资源调度的时候,是存在资源竞争的,所以 取内存对象的时候需要加锁。但是这个采用的时候桶锁,所以只要当多个线程竞争同一个桶中的资源的时候才会加锁,而且是由threal cache
没有内存对象的时候才会申请资源,所以这个内存申请资源不会很激烈。page cache
:页缓存是在central cache
缓存上面的一层缓存,存储的内存是以页为单位存储以及分配的。 当central
没有缓存的时候,从page cache
中分配出一定数量的page
并且并且切割成定长大小的小块内存,分配给central cache
。当central cache
中一个span
的几个跨度页的对象都回收回来之后,page cache
会回收central cache
中满足条件的span
对象并且会合并成相邻的页,组成更大的页,缓解了内存碎片的问题。thread cache
整体设计前面定长内存池使用自由链表的结构来分配内存,但是链表中的节点都是定长的。为了适应不同长度的内存块分配情况,可以使用多个连接着不同字节大小的内存块的链表。
但是thread cache
中最大的内存块是256KB
,如果我们为了精确分配的内存的话,需要使用256*1024
个链表(256KB=256×1024B)的话就太浪费了。所以我们可以使用8B
,16B
,24B
…256KB
这样粗略地分一下即可,在申请资源的时候是要去大于等于当前申请内存的最小内存块即可 (按照一定大小进行内存对齐)。
这样设计缺点在于可能会有很多的空间浪费,造成内存碎片,并且是内碎片。
另外thread cache
采用哈希桶结构,每一个桶中是按桶的大小去映射的,即桶中的自由链表的内存块对象大小等于桶大小,使用哈希映射可以快速得到线程星想要得到的内存块的大小。这样设计使得每一个线程都有一个一个thread cache
对象,每一个线程获取对象和释放对象时是无锁的。
问题1:处理哈希桶中自由链表问题。
由于每一个哈表桶中都需要挂一个自由链表,所以可以将自由链表封装成一个类专门管理小内存块。
// 统一写法,取出一块内存头部的4/8个字节存放下一个内存块的地址
void*& NextObj(void* obj)
{
return *(void**)obj;
}
// 管理切好的小块内存的自由链表
class FreeList
{
public:
// 采用头插
void Push(void* obj)
{
// 如果obj为nullptr则不能插入
assert(obj);
// 头插内存块
//*(void**)obj = _freeList;
NextObj(obj) = _freeList;
_freeList = obj;
}
// 采用头删
void* Pop()
{
// 如果_freeList为nullptr则不能删除
assert(_freeList);
// 头删内存块
void* obj = _freeList;
_freeList = NextObj(_freeList);
return obj;
}
private:
void* _freeList;
};
class ThreadCache
{
public:
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
private:
// 问题1
};
问题1:给一个需要内存块的大小
size
,怎么将这个内存块对齐呢?
使用一个类专门来管理和计算对象大小的内存对齐的映射规则。其中至少要按8字节对齐,因为64平台下一个指针都8字节。但是如果256KB都按8字节对齐的话,需要3万多个哈希桶,所以可以进一步的改造一下,每一个字节范围内按一个字节数来对齐。
这样就可以控制最多10%左右的内存碎片浪费。前期的对齐数小一点,后面的对齐数变大。
// "common.h"中
// 最大的自由链表数量
static const size_t NFREE_LISTS = 208;
// threadcache中最大分配的内存块的大小
static const size_t MAX_BYTES = 256 * 1024;
class SizeClass
{
public:
static inline size_t _RoundUp(size_t size, size_t alignNum)
{
// 将size按alignNum对齐数对齐
return ((size + alignNum - 1) & ~(alignNum - 1));
// 也可以这样
//return (size + alignNum - 1) / alignNum * alignNum;
}
// 为了保证在类外可以直接调用函数,而不是使用对象调用函数
// 所以可以将函数设置成static的
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
// 分配的内存不能大于256KB
assert(false);
return -1;
// 其实如果超过256KB也是可以申请的,后面会讲
}
}
static inline size_t _Index(size_t size, size_t align_shift) {
// 其实就是size/2^(align_shift)上取整然后-1
return ((size + (1 << align_shift) - 1) >> align_shift) - 1;
}
// 计算自由链表所在哈希桶中的位置
static inline size_t Index(size_t size)
{
// 每一个区间中有多少的自由链表
static int group_array[4] = { 16, 56, 56, 56 };
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[0] + group_array[1];
}
else if (size <= 64 * 1024)
{
return _Index(size - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
else if (size <= 256 * 1024)
{
return _Index(size - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
}
else
{
assert(false);
return -1;
}
}
};
// "ThreadCache.h"中
class ThreadCache
{
public:
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从centralcache中获取内存
void* FetchFromCentralCache(size_t index, size_t size);
private:
// 用数组模拟哈希表,最多有NFREE_LISTS
FreeList _freeLists[NFREE_LISTS];
};
// ”ThreahCache.cpp“中
void* ThreadCache::Allocate(size_t size)
{
// threadcache最多只能分配256KB
assert(size <= MAX_BYTES);
// size对齐之后的字节数
size_t alignSize = SizeClass::RoundUp(size);
// size字节数对应的哈希桶的位置
size_t index = SizeClass::Index(size);
// 如果申请内存大小对应的哈希桶中的自由链表为空,就去centralcache中拿
// 否则直接从自由链表中获取即可
if (_freeLists[index].Empty())
{
return FetchFromCentralCache(index, alignSize);
}
else
{
return _freeLists[index].Pop();
}
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
// ...
}
问题0:什么是TLS?
TLS(线程局部存储),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保证了数据的线程独立性。
问题1:为什么需要
TLS
?
为了保证每一个线程都可以有自己专属的thread cache
,所以可以使用TLS
,来保证每一个线程都可以无锁地获得自己的thread cache
对象。TLS分为静态和动态的,使用静态的LTS最简单,只需要声明一个_declspec(thread)
的变量就会给每一个线程单独的一个拷贝。
问题2:"ConcurrentAlloc.h"是什么作用?
这里需要专门准备两个函数给每一个线程调用分配内存。
问题3:
.h
文件中很多的static
修饰的变量和函数是为什么?
static修饰函数,改变链接属性,一个.h文件中可以被多个.cpp文件包含,所以这里使用static保证其中的static的变量或者函数只保存一份,这样就不会再生成.obj文件的时候相互冲突了,static保证了变量或者函数只在当前文件可见 。
// "ThreadCache.h"中
// 问题1
// TLS,保证每一个线程的独立
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
// "ConcurrentAlloc.h"中
// 问题3 && 问题2
static void* ConcurrentAlloc(size_t size)
{
// 通过TLS,每一个线程可以获得自己专属的ThreadCache对象,并且这个过程是无锁的
// 如果ThreadCache的自由链表中节点的话,那么效率则非常高
if (pTLSThreadCache == nullptr) {
pTLSThreadCache = new ThreadCache;
}
cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;
return pTLSThreadCache->Allocate(size);
}
// 这里需要将内存归还给threadcache中,所以需要知道对应哈希桶中归还的自由链表的位置
// 但是concurrentFree()不应该有size,这里暂时这样写
static void ConcurrentFree(void* ptr, size_t size)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
central cache
的整体设计申请内存的过程
central cache
也是一个哈希桶的结构。并且因为所有的线程如果自己的thread cache
中没有内存的话,就都需要从central cache
中获取,因此central cache
需要加锁,但是是桶锁,也就是每一个哈希桶中都有一个单独的锁。central cache
也有一个哈希映射的spanlist
,spanlist
中挂着span
,从span
中取出对象给thread cache
这个过程也是需要加锁的。
central cache
映射的spanlist
中的所有span
都没有内存之后,然后需要从page cache
中申请一个新的span
对象,拿到span
之后将span
管理的内存大小切好作为自由链表连接到一起。
central cache
中挂的**span
中的use_count
记录分配了多少个对象出去。**
释放内存
thread cache
归还内存的时候(可能是自由链表过长或者线程销毁了)则会将内存释放回central cache
中(为了形成”均衡调度“(优点1)。),释放回来时span
中的use_count --
。当所有的对象都回到span
中的,则span
释放回到page cache
,page cache
中会对前后相对空闲页进行合并。(缓解了外碎片问题(优点2))central cache
结构设计central cache
中是以span
为单位分配内存的,如果一个span
中的所有节点内存都分配完了之后,central cache
的哈希桶中就会申请新的span
。
span
是管理多个连续页大块内存跨度结构。
问题1:在32位和64位不同的平台下,页数是不同的
因为32位下最多只有 2 32 / 2 13 = 2 1 9 2^{32}/2^{13}=2^19 232/213=219个页(一页是8KB),但是64位平台下有 2 64 / 2 13 = 2 51 2^{64}/2^{13}=2^{51} 264/213=251个页,因此需要使用不同的类型保存页的个数。
所以我们要是要条件编译,并且x86下只有_WIN32,而x64下既有_WIN64也有_WIN32,因此我们需要先判断_WIN64,才可以适应两个平台
问题2:哈希桶中的结构
哈希桶中有不同的页,由span
为单位,每一个span
下都挂着一个自由链表。并且span
之间形成一条链表。因为当span
下没有可以使用的对象的时候,就需要回收,因此对于链表中的节点需要容易删除,所以采用带头双向循环链表的结构组织SpanList
。
问题3:哈希桶中的锁怎么实现?
因为central cache
可能会有很多的线程同时的竞争,所以我们需要上锁。但是只有多个线程竞争同一个哈希桶中的span
中的内存的时候,才需要上锁,所以我们就可以在SpanList
中加上锁。
// 问题1
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID; // size_t->unsigned int
#else
// linux下
#endif
struct Span
{
// 问题1
PAGE_ID _pageId = 0; // 大内存起始页的页号
size_t _n = 0; // 页数量
Span* _next = nullptr; // Span双向链表结构
Span* _prev = nullptr;
size_t _usecount = 0; // span中的对象个数
void* _freeList = nullptr; // span中自由链表
bool _isUse = false; // span是否正在被使用
};
// 问题2
// 带头双向循环
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
// 将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;
}
// 将span从双向链表中删除
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
pos->_prev->_next = pos->_next;
pos->_next->_prev = pos->_prev;
// 这里可以不用删除这个节点,因为当span中没有节点的时候就会自动的归还给pagecache
// delete pos;
}
private:
Span* _head;
// 问题3
std::mutex _mtx; // 桶锁
};
central cache
核心实现问题1:线程怎么保证只从一个
central cache
取用内存?
通过TLS
指针可以使得每一个线程都有一个独立的thread cache
。而所有的thread cache
都需要从一个共享的central cache
中取用内存,所以需要保证一个程序中只有central cache
对象,因此要对这个类实行单例模式。
问题2:每一个哈希桶中的
span
的中的内存块每一个从central cache
中拿几个?
如果一次只拿一个,那么可能下次还需要再拿,如果遇到了两个线程同时竞争资源的话就会降低效率。但是也不能一次性给很多的内存块,否则用不完的话就会浪费。而且哈希桶中的大内存块一次性给10个和小内存块一次性给10个概念也是不同的,例如256KB对象的span
可能不用着10个,但是可能8B的内存块中对象10个还不够。因此可以采用慢启动的方法平衡一下内存块分配的数量。
问题3:如何从
central cache
中的span
中获取内存到thread cache
中?
首先要从central cache
上获取一个span
(后面会讲解如果获取span
),然后从span
中获取batchNum
个内存块,但是可能一个span
上的内存块不足batchNum
,那么有几个内存块就获取几个内存块。注意这个从central cache
中的哈希桶中获取span
和从span
获取内存是需要加锁的。用于保持线程之间互斥性。
问题4:获得actualNum个内存块的实现过程
因为end
表示指向分配内存的最后一块内存对象的指针。所以需要前提预备一块内存。即actualNum
初始化值为1。也就是end
最后只会链表中的最后一块内存,也就是当NextObj(end) == nullptr
的时候。
// 在SizeClass中算出span中自由链表分配内存的上限
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// 根据size的大小决定内存块num的个数
size_t num = MAX_BYTES / size;
if (num < 2) {
num = 2;
}
if (num > 512) {
num = 512;
}
return num;
}
// 从centralcache的哈希桶中的链表中的span获得内存块
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// 问题2
// 慢开始反馈调节算法
// SizeClass::NumMoveSize(size)算的是这个自由链表中内存块分配的上限(根据不同的size决定上限的大小)
// _freeLists[index].MaxSize()算的是span中自由链表中的下限,并且每一次触及下限的时候会不断的增加下限
size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;
}
// [start,end]是centralcache分配的空间
// start和end都是输出型参数
void* start = nullptr;
void* end = nullptr;
// 实际从centralcache中获得内存块对象的数量
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum >= 1);
// 如果内存块数量等于1,直接返回
// 否则就返回一块给用户,其余的内存块保留在threadcache中
if (actualNum == 1) {
assert(start == end);
return start;
}
else {
_freeLists[index].PushRange(NextObj(start), end);
return start;
}
}
// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// ...
}
// 问题3
// 从central cache中获取一定数量的对象给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();
// 从central cache中获取一个span
Span* span = GetOneSpan(_spanLists[index], size);
// 确保span中一定有_freeList(中的内存块)
assert(span);
assert(span->_freeList);
// 获取span中的节点
start = span->_freeList;
end = start;
// 如果span中的对象不够batchNum的话,span中有多少对象就给多少
size_t i = 0;
size_t actualNum = 1;
// 问题4
while (i < batchNum - 1 && NextObj(end))
{
end = NextObj(end);
i++;
actualNum++;
}
span->_freeList= NextObj(end);
NextObj(end) = nullptr;
// usecount为span中申请出去的小内存块的个数
span->_usecount += actualNum;
_spanLists[index]._mtx.unlock();
return actualNum;
}
page cache
的整体设计central cache
需要按哈希桶的映射规则
问题1:
pagecache
的整体设计。
page cache
也是一个哈希桶,哈希桶中挂着的也是span
,而哈希桶中映射规则和central cache
,thread cache
不同,不是根据对齐的字节数来分配的,而是根据不同的page
大小来分配的,在[1page, 128page]
之间,最大只能按128页为单位分配内存。page cache
也是所有的线程都共享的一个结构,所以需要使用单例模式。central cache
因为不同的线程需要到不同的哈希桶中对应不同的字节数中取用节点,因此需要使用桶锁。但是**page cache
需要对整个page cache
进行上锁**。因为像page cache
获取内存会设计到多个哈希桶中的内存一起变化,所以需要对整个page cache
都上锁。
page
哈希桶下没有span
的话,那么就去从现存在page cache
的大page
下分割大page
然后分成当前的需要的page
,也就是一开始什么都没有的时候,page central
应该会先去堆中申请一个128page
大小的span
,然后慢慢分割。正是这种对page central
都需要多个线程去申请,所以需要对整个page cache
整体上锁。(如果使用桶锁的话,就需要在有大页分割成小页的过程中就会不断的加锁解锁,这样通过在不同的哈希桶中不断切换的方式来获取资源的方式,如果使用桶锁就会带来巨大的性能消耗)page cache中的内存回收机制
如果central cache
中的**span
的usecount
等于0**,说明分给thread cache
的小块内存对象都还回来到了span
中,则central cache
那这个span
还给page cache
,page cache
通过页号,查看前后的相邻页是否存在空闲,如果是空闲的话就合并两个页形成一个更大的页,缓解了内存碎片的问题。
// pagecache的哈希桶中桶的个数
static const size_t NPAGES = 128;
// 问题1
class PageCache
{
public:
// 单例模式(饿汉模式)
PageCache* GetInstance()
{
return &_sInst;
}
// 获取一个K也的span
Span* NewSpan(size_t k);
private:
SpanList _spanLists[NPAGES];
private:
PageCache() {}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
// pagecache中使用一个大锁可以锁住整个pagecache,而不是桶锁
// 因为这个时候不同页对应的哈希桶中已经产生了相互的关系
std::mutex _pageMtx;
};
page cache
中获取Span
上问题1:可以从哪些地方获取Span?获取Span的顺序?
如果想要获取一个Span
的话,可以从从centralcache
的哈希桶中的SpanList
中获取Span
。也可以向pagecache
获取Span
。
我们应该先在centralcache
的哈希桶中的SpanList
中查看是否存在Span
。如果centralcache
本身就有Span
,就不用向系统申请了。如果没有,就需要从pagecache
中再获取。
问题2:如何计算一次系统获取几个页内存?
计算出size
大小的节点需要的batchNum
(自由链表中的内存节点数量)的上限。利用页数计算出字节数然后换算成页数。
问题3:获取Span后,需要堆Span做哪些处理?
因为从pagecache
中获取的是NumMovePage(size)
大小的的内存块,所以这个内存块是连续整块的。而threadcache
中需要的是大小为size
的内存对象,所以需要对从pagecache
获取的span
进行切割。
span
个数计算出span的起始地址和内存大小。
char*
接收。是为了后面方便切分内存。span->_freeList
下
span
放入centralcache
对应的哈希表的SpanList
双链链表中// "common.h"中
// 计算一次项centralcache申请的页数的上限
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// 根据size的大小决定内存块num的个数
size_t num = MAX_BYTES / size;
if (num < 2) {
num = 2;
}
if (num > 512) {
num = 512;
}
return num;
}
// 计算一个向系统申请的页数
static size_t NumMovePage(size_t size)
{
// 问题2
size_t batchNum = NumMoveSize(size);
size_t npage = batchNum * size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
// central cache向pagecache要一个span
// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 问题1
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
it = it->_next;
}
// 走到这里说明当前的spanlist中没有空闲的span了,需要找page cache要
// 问题2
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
// 问题3
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t bytes = span->_n << PAGE_SHIFT;
char* end = start + bytes;
span->_freeList = start;
void* tail = span->_freeList;
start += size;
while (start < end)
{
// 尾插内存节点到span的自由链表中
NextObj(tail) = start;
tail = start;
start += size;
}
// 将切分好的span放入哈希桶中
list.PushFront(span);
return span;
}
page cache
中获取Span
中上面说的是假设central cache
已经从page cache
中获取一个span
了,如何处理这个大块的内存(以页数为单位)。
下面要说的是
page cache
如何分配给central cache
k页的span
。
问题1:对于
central cache
的桶锁和page cache
的整个锁怎么处理?两者之间的关系?
整个page cache
都加锁(因为可能小页数的page
的哈希桶会向大页数的page
的哈希桶要切割下来的page
大内存)。在page cache
整体加锁的时候,这个时候central cache
的桶锁最好解掉。
虽然当申请span
的线程还是需要等待申请span
(因为page cache
被锁住一定是内存不够用),但是此时如果有central cache
中的线程归还内存的话就会降低归还的效率了。
当线程向pagecache
申请Span
的时候,可能会有其他向centralcache
申请内存的线程,所以为了不影响在等待从pagecache
中申请内存的效率,因此可以在一个线程向pagecache
申请span
的时候,将centralcache
的桶锁解掉。
所以需要在centralcache
的GetOneSpan
中的向page cache
的NewSpan
前将锁解掉。并且为了显式地证明page cache
是整体锁住的,所以可以直接在NewSpan
外面用所锁住。在得到page cache
给的span
后,可以不用着急的用锁锁住,因为当前的Span
不在哈希桶的双链表(双链表是临界资源)中,所以不可能会出现多线程竞争的问题,这个Span
只能被当前线程访问,所以可以等到span
被切分之后插入到_spanList
中的时候会对申请_spanList
的线程竞争的时候需要将锁在锁住。
并且根据NewSpan
中的递归逻辑,可以知道只能将锁加在调用NewSpan
函数的外面,而不能在NewSpan
内部加锁。 因为如果在NewSpan
内部加锁的话,就会使得相同锁因为递归调用被锁了两次,最终导致死锁的现象。(在C++11中有recursive_mutex
可以解决在递归调用时递归使用锁的问题)
问题2:怎么从
page cache
中获取span
?
有三步需要考虑:
page cache
的哈希桶中存在k
页的span
的话直接返回k
页的span
和n-k
的span
。span
,就直接向堆申请,然后重复执行一遍切割这个大内存的逻辑。// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 查看当前Span下的SpanList中是否还有未分配对象的span
// 如果有就直接返回span,如果没有就需要从page cache中再获取
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
it = it->_next;
}
// 问题1
// @@先将central cache的桶锁解掉,这样的话如果有其他的线程内存释放对象的话不会阻塞
list._mtx.unlock();
// @@对page cache整体加锁
PageCache::GetInstance()->_pageMtx.lock();
// 走到这里说明当前的spanlist中没有空闲的span了,需要找page cache要
// 这里是size传入NumMovePage()中后,就可以根据size算出需要的内存块的节点数量的上下
// 进而推导出需要的页数
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
PageCache::GetInstance()->_pageMtx.unlock();
// 将span进行切分
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t bytes = span->_n << PAGE_SHIFT;
char* end = start + bytes;
span->_freeList = start;
void* tail = span->_freeList;
start += size;
while (start < end)
{
// 尾插内存节点
tail = NextObj(tail) = start;
start += size;
}
// @@在span切割的时候不用加锁,因为其他线程在外函数外面不能访问到span
// @@而下面需要在list中插入span的时候会对向centralcache申请的线程造成影响
// @@所以这个时候需要加锁
list._mtx.lock();
list.PushFront(span);
return span;
}
// "PageCache.cpp"中
Span* PageCache::NewSpan(size_t k)
{
// 问题2
assert(k > 0 && k < NPAGES);
// 判断是否存在k页的span
if (!_spanLists[k].Empty())
{
return _spanLists[k].PopFront();
}
// 如果哈希桶中不存在k页的span的话
// 就需要将大的span切分成k页的span和n-k页的span
// 将k页的span返回,然后将n-k页的span直接挂在n-k页的哈希桶中
for (size_t i = k + 1; i < NPAGES; i++)
{
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
// 从nSpan的头部切出kSpan来,返回kSpan,重新挂nSpan
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
_spanLists[nSpan->_n].PushFront(nSpan);
return kSpan;
}
}
// 走到这个位置说明后面没有更大的页了
// 这时就只能向堆申请了
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
// 将地址转换成页号
bigSpan->_pageId = (PAGE_ID)ptr >> NPAGES;
bigSpan->_n = NPAGES - 1;
// 将大内存插入到第NPAGES-1号桶中
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
%%# 14 申请内存过程联调
从堆中申请的内存的地址可以转成span->_pageId
页号,这样通过页号可以找到这块地址的地址了。
测试一:
void TestConcurrentAlloc1()
{
void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(6);
void* p5 = ConcurrentAlloc(8);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
cout << p4 << endl;
cout << p5 << endl;
}
第一次申请内存,在ThreadCache
中没有内存,所以去Central Cache
中的span
中要,但是Central Cache
中index
位置没有span
,所以此时就要从Page Cache
中第k
个哈希桶中获得span
。此时pagecache
中第k
个哈希桶中也没有span
,所以就需要从系统中获取一个128
页的内存挂在哈希桶中的第128的位置上。然后将128位置上的内存块切成n-k
页的内存块和k
页的内存块。将k
页的span
返回
central cache
拿到span
后将span
分成k页数的字节/size
块小内存
,然后拿其中batchNum
个内存块。并且返回给ThreadCache
,并且将后面没有使用的小内存块放在第index
号哈希桶中。最后用threadcache
返回给用户想要的得到的内存块。
第二次申请内存的时候,还是相同的逻辑。但是因为central cache
中有span
所以可以直接从span
中切出batchNum
个小内存块给threadCache
。(注意这里只需要一个但是由于上次一次也是分配8字节内存,这次也是分配8字节内存,所以这次因为慢增长会获得2个内存块。
第三次因为上一次慢增长,所以这一次直接从threadcache
中获得内存块。
测试二:
void TestConcurrentAlloc2()
{
for (int i = 0; i < 1024; i++)
{
void* ptr = ConcurrentAlloc(8);
}
void* p = ConcurrentAlloc(8);
}
因为第一次会从page cache
中的128号桶中获取分配给central cache
一页的内存,但是在分配1024
个8
字节内存块的时候就会重新要从page cache
中获取span
了。
总结:
thread cache
actulNum
个内存块给用户Allocate
:获取size
字节大小的内存FetchFromCentralCache
:通过慢增长获得内存块数量central cache
GetOneSpan
:返回一块已经切分好的span其中的一块。如果span没有切分好(因为使用page cache中刚获取的),可以切分好再归还。FetchRangeObj
:将span
切分好的自由链表中获取batchNum
块。page cache
NewSpan
:返回k
页的span,如果有就返回。如果没有就看看是否可以从更大页的span分割。如果没有更大的span就直接从系统要128页的内存。%%为什么需要
threadcache
回收内存?
当thread cache
的哈希桶中保留的自由链表的的长度过长的话,因为使用不上,所以可以进行回收。
tcmalloc
中的机制:
tcmalloc
:使用链表的长度和内存的大小来判断是否回收。
在这个项目中简化一下:
如果归还的内存块超过了一次批量(_freeLists[index].MaxSize()
)的内存块的长度,就整体做出归还。
问题1:什么时候将哈希桶中的自由链表中的节点回收?
当_freeLists[index].Size() == _freeLists[index].MaxSize()
的时候,就会因为第index
号桶中的自由链表的长度过长而导致需要回收。此时并不是将所有的size
字节大小的内存全部收回,而只是将其中MaxSize
块回收。
tcmalloc
中的回收机制其实处理的很细致,所以很复杂。
还可以控制哈希桶中整体申请内存的大小,如果所有申请的内存大小超过2M的话,就从centralcache
的多个哈希桶中回收内存。
可以使用一个变量记录thread cache
中整体申请的剩余内存的大小,如果大于2M的时候,就可以对哈希桶中的各个桶中的链表进行一个清扫。
问题3:如果自由链表太长怎么回收?
可以增加接口回收_freeLists[index]
中的n
个内存块。在FreeList
类中增加接口。
问题4:将自由链表中的内存块怎么处理?
将自由链表中的内存块归还给块给central cache
中的span
,因为在central cache
需要有可以接收从thread cache
内存块放到span
中的接口。
// "ThreadCache.cpp"中的回收内存接口
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 找到对应的自由链表哈希桶,将内存块回收回_freeList[index]中
size_t index = SizeClass::Index(size);
_freeLists[index].Push(ptr);
// 问题1
// 如果_freeList[index]中自由链表的长度过长(大于一次批量申请的内存)的话,就需要回收给central cache
if (_freeLists[index].Size() > _freeLists[index].MaxSize())
{
ListTooLong(_freeLists[index], size);
}
}
// 释放对象时,链表过长,回收内存回中心缓存中
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
// 将一批数量的内存块拿出
void* start = nullptr;
void* end = nullptr;
// 问题3
list.PopRange(start, end, list.MaxSize());
// 问题4
// 以自由链表的形式还给central cache中
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
// "Commom.h"中FreeList的PopRange接口
void PopRange(void*& start, void*& end, int n)
{
assert(n >= _size);
// 将n个内存块从自由链表中移出
start = _freeList;
end = start;
for (int i = 0; i < n - 1; i++)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
问题0:如何将小块内存还给对应的
span
?
从thread cache
中回收的内存块组成的链表,它们每一个内存块原来所属的span
都不确定。
因为小块内存是从span
中切来的,所以可以通过计算得到小块内存的页号,而在同一个span
下的小块内存下计算的页号是相同的(因为地址会整除)。这样就可以使得所有的小块内存找到自己对应的span
了。
因为同一个Span
中的小块内存对象的页号都是相同的。所以可以通过建立页号和Span
的对应关系方式使得在只有小块内存地址的情况下,找到其对应的Span
。
我们可以根据小块内存的地址,可以计算出这个小块内存的页号。方法:属于同一个span
的内存块由于是连续的,所以这些内存块的地址/8K
之后应该相同,并且等于span->_pageId
。
问题1:如何将
span*
和页号进行映射?
因为后面page cache
回收内存的时候也可以使用建立映射相同的映射关系,所以unordered_map
放到page cache
类中,使用page cache
的单例调用这个容器,这样在centralcach
和pagecache
中就都可以使用映射关系了。
而且在NewSpan
中需要将每一页和span进行映射。
问题2:如何处理回收的小块内存?
将小块内存找到对应的span
,并将它们头插到span
中。并且每当一个小块内存归还的时候,该span
对应的usecount
就需要减去1。当usecount
减到0的时候,就需要将该span
归还给pagecache
。
问题3:当一个
Span
中的所有小块内存都已经归还回来后(即Span
的_usecount
等于0的时候)怎么办?
当usecount==0
的时候Span
中已经是原来的一段连续空间了,所以需要将span
归还给pagecache
。
归还方法:将该span
从_spanList
中剥离下来,调用ReleaseSpanToPageCache
交给pagecache
处理。
// "PageCache.h"
// 问题1
// 将页号和Span*建立一个映射
std::unordered_map<PAGE_ID, Span*> _idSpanMap;
// "PageCache.cpp"
// 问题1
// 将span和每一页的地址进行一个映射
// 方便central cache回收小块内存时,找到小内存块和span的对应关系
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
// "PageCache.cpp"
// 问题1
// 获取小块内存和span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
// 将小块内存的页号算出,然后返回该页号对应的span
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
if (_idSpanMap.count(id))
{
return _idSpanMap[id];
}
else
{
// 如果程序正确,应该一定会有对应的span
assert(false);
return nullptr;
}
}
// "CentralCache.cpp"
// 将一定数量的小内存块归还给central cache的span
// 需要归还[start, end]这一段空间,但是因为end后面已经手动地指向空了,所以可以只传start这个参数即可
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
// 使用桶锁
_spanLists[index]._mtx.lock();
// 将回收的小块内存插入到对应的span中
while (start)
{
// 问题2
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
// 将小块内存头插到span中
NextObj(start) = span->_freeList;
span->_freeList = start;
start = next;
span->_usecount--;
// 问题3
// 当span的usecount等于0的时候,说明span切出去的所有小块内存都已经回收
// span整个回收回pagecache中,然后pagecache可以进行前后也的合并
if (span->_usecount == 0)
{
// 将span从这个哈希桶的index位置上剥离下来
_spanLists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
// 解除桶锁,因为当前线程已经不在对当前哈希桶中的自由链表造成影响了
// 所以为了不影响其他的线程申请和归还内存,这样需要解除桶锁,加上pageMtx
_spanLists[index]._mtx.lock();
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.unlock();
}
}
_spanLists[index]._mtx.unlock();
}
问题0:
pagecache
回收Span
的机制
如果page cache
将从central cache
回收回来的span
直接挂在哈希桶的第k
个位置上的话,那么最后就是一个由span
组成的链表,这样还是没有缓解内存碎片的问题。
只有对span
前后的页,尝试进行合并,使得前后的Span
可以连接起来,最终能所有的内存可以连续,这样才能缓解外碎片问题。
问题1:
span
前后合并空闲页需要的条件?
合并的过程中是一个循环的过程。其中需要满足三个条件。
span
可以和前面的span
合并的话,那么在合并之后需要再判断是否还可以再往前合并。直到前面的span
不能再合并span
NPAGES-1
(128页)为止。问题2:如何合并前后两个
Span
?
通过查找一个页的前后的相邻页(前提是相邻页是空闲页,也就是在page cache
中剩余的内存页),如果相邻页是空闲的话可以尝试进行合并。
使用unordered_map
找到相邻页对应的Span
,然后修改Span
的属性就相当于合并相邻页了。
合并完相邻页之后,就可以将Span
放入pagecache
中对应大小的哈希桶中了。
问题3:如何比较,合并前后两个
span
?
Span
前面一个prevSpan
的页号,即prevSpan->_pageId
,等于span->_pageId-1
。因为span*
是Span
的开头,所以前面一个页就是前一个Span
。
但是Span
后面一个nextSpan
的页号,等于span->_pageId + span->_n
。因为span
的页数可能会有很多,所以加上span->_n
之后,才是prevSpan
的首地址。
合并span
前面的空闲页只需要修改span
中的属性信息即可。即让span
去管理prevSpan
/nextSpan
的自由链表,并且修改对应的页数量,首地址,是否使用等信息。
在合并相邻空闲页之后,由于span
已经接管了相邻页。所以相邻页需要从原来pagecache
的哈希桶中的位置剥离下来。将合并过后的span
放入对应的哈希桶中。最后将管理相邻页的prevSpan
/nextSpan
销毁掉
在合并完相邻页之后,需要对span
进行映射关系的处理,将其span
和pageId
也进行映射。
小提示:
每一个内存块都由自己的Span
管理,为了找到自己的Span
所以也可以建立和Span
的映射关系。所以处理每一个内存的时候,都需要修改管理自己Span
的属性信息。并且要建立内存块和自己Span
的映射关系。
问题4:如何找到一个
span
是否为空闲页,即span
是在pagecache
中还是在centralcache
中呢?
不能使用usecount
判断,因为会导致线程不安全。
因为pagecache
在锁住的时候,centralcache
的哈希桶中并没有锁的限制。所以一个在回收并且合并Span
的时候,可能会有另一个线程在开辟一个新的Span
,这个时候新的Span
的usecount
也是等于0的。所以可能会出现一个线程刚刚开辟的Span
被另一个线程回收合并了。
因为需要给span
加一个属性_isUse
,_isUse
专门用于判断这个span
是否真正被使用。如果_isUse == true
的话,说明当前的Span
不能回收。
问题5:如何将
pagecache
中所有的的span
进行和页号映射?
如果我们需要对所有空闲的span
的都需要回收的话,就需要对所有的空闲span
都建立和页号的映射。但是前面我们为了将所有小内存块和span
进行回收的时候只讲小内存块和span
进行映射,而从大内存分下来的其余部分的内存并没有建立映射。所以我们需要建立两者的映射。但是也没有必要向需要分配给central cache
的内存一样切分的那么细致,而只需要将内存的收尾页建立映射即可,因为当一个span
找空闲页的时候,只会找前后相邻的两页,所以只需要对在pagecache
中的空闲页的收尾两页的内存上建立和span
的映射即可。
// 存储nSpan的收尾页号和nSpan的映射,方便回收pagecache内存时候进行合并
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 问题0
// span回收回PageCache中,并且合并相连的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 对span前后的页,不断尝试进行合并
// 问题1
// 向前合并
while (true)
{
// 问题2&&问题3
PAGE_ID prevId = span->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
// 如果前面的内存没有分配空间形成span就不合并
if (ret == _idSpanMap.end())
{
break;
}
// 问题4
// 如果当前的span正在centralcache中使用的话,不能合并
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}
// 如果前面页和当前页合并后超过128页太大了,不合并
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
_spanLists[span->_n].Erase(span);
// 问题5
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
// 将prevSpan从pagecache的_n的位置上剥离下来
_spanLists[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
// 向后合并
while (true)
{
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;
}
_spanLists[span->_n].Erase(span);
span->_n += nextSpan->_n;
_spanLists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
// 问题5
// 最后将span放入_n的哈希桶桶中即可
// 并且记录span的pageId和span的对应关系
_spanLists[span->_n].PushFront(span);
span->_isUse = false;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}
%%# 释放内存过程联调
可以测试ConcurrentAlloc()
7次,然后再ConcurrentAlloc
7次,这样正好就可以将原来将pagecache
中128页切割成的127页和1页全部的还回去了。
因为前面分配了内存7次内存,所以这些内存应该都是从一个1页的span
中获得的(因为1页的span
可以切成1024块大小为8bytes的小内存块)
到了第7次释放的时候,首先是小内存块全部都还给span
中了,所以span
就可以将所有的小内存块合并,并且往上交给pagecache
了。
由于这1页的span
(从centralcache
中剥离下来的)原本是由128页的内存块切成的127页和1页中的span
,所以在合并的时候,这个span
往后就可以找到pageId=127
的span
进行合并。最终threadcache
和centralcache
中都没有剩余的内存块了,而pagecache
中有一个128页的内存块。%%
问题0:大于
256KB
的内存的申请的设计方案。
pagecache
)threadcache
,centralcache
,pagecache
共同完成即可。pagecache
中以页为单位申请内存。pagecache
也不能分配内存了,这样时候最好就直接向系统堆中申请。问题1:怎么统一处理大于32页小于128页的内存和128页的内存的申请?
因为大于32页小于128页的内存申请还是向pagecache
申请一个span
,因此直接可以跳过去threadcache
和centralcache
申请的部分,直接向pagecache
申请一个span
,所以可以算出这个申请内存的页数,然后向pagecache
要内存即可。
为了统一的调用接口,大于128页的内存也可以当做一个span
向pagecache
申请。而在NewSpan
中可以特殊这里超过128页内存的申请,可以直接调用SystemAlloc
向系统调用。
问题2:怎么统一处理大于32页小于128页的内存和128页的内存的释放?
因为大于32页小于128页的内存释放还是将span
归还给pagecache
,而且可以和前后span
的空闲页合并。为了统一接口,也可以将归还超过128页内存的过程看成归还一个span
给pagecache
。但是实际上可以使用SystemFree
将内存直接归还给系统。
// 问题0
static void* ConcurrentAlloc(size_t size)
{
/->
// 大于256KB的时候,需要特殊处理
if (size > MAX_BYTES)
{
size_t alignSize = SizeClass::RoundUp(size);
size_t kpage = alignSize >> PAGE_SHIFT;
// 问题1
// 这里会访问pagecache的哈希桶(临界资源),所以需要上锁
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);
PageCache::GetInstance()->_pageMtx.unlock();
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
return ptr;
}
/<-
else
{
// 通过TLS,每一个线程可以获得自己专属的ThreadCache对象,并且这个过程是无锁的
// 如果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)
{
//->
if (size > MAX_BYTES)
{
// 问题2
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);
}
}
// 问题1
// 大于的128页的内存页放在这里,就是为了可以让这个函数统一处理分配span的问题
// 如果小于128页的span可以从这里拿,大于128页的span也可以从这里拿
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;
}
// 问题2
// 大于128页的内存直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
SystemFree(ptr);
delete span;
return;
}
在tcmalloc
中不应该出现new
,malloc
等关键字,因为tcmalloc
本身设计出来就是要和malloc
相比较的。
所以我们可以使用前面写的一个定长的内存池,去替代new
的作用。
因为只有pagecache
的哈希桶中需要有一个双向带头循环的链表,所以需要new
一个Span
,因为可以在SpanList
中定义一个ObjPool
专门用于new
其中的Span
。
除此之外,在ThreadCache
中还需要new
一个ThreadCache
给TLS,可以定义一个static
的ObjPool
保证全局只有一个定义ThreadCache
的内存池。
测试多线程下malloc
和ConcurrentAlloc
的性能。
// ntimes:一轮申请释放内存的次数
// rounds:释放的轮数
// nworks:创建的线程数
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++)
{
// nworks个线程依次执行lambda表达式
// lambda表达式中执行rounds轮线程申请内存和释放内存的过程
// 每一轮的都申请ntimes次内存和释放ntimes次内存
vthread[k] = std::thread([&]() {
std::vector<void*> v(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));
}
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& e : vthread)
{
e.join();
}
printf("%u个线程并发执行%u轮次,每轮次malloc%u次,花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次free%u次,花费:%u ms\n",
nworks, rounds, ntimes, free_costtime);
printf("%u个线程并发执行malloc&free %u轮次,总花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime + free_costtime);
}
// ntimes:一轮申请释放内存的次数
// rounds:释放的轮数
// nworks:创建的线程数
void BenchmarkConcurrentAlloc(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++)
{
// nworks个线程依次执行lambda表达式
// lambda表达式中执行rounds轮线程申请内存和释放内存的过程
// 每一轮的都申请ntimes次内存和释放ntimes次内存
vthread[k] = std::thread([&]() {
std::vector<void*> v(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));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
ConcurrentFree(v[i], 16);
}
size_t end2 = clock();
v.clear();
malloc_costtime += end1 - begin1;
free_costtime += end2 - begin2;
}
});
}
for (auto& e : vthread)
{
e.join();
}
printf("%u个线程并发执行%u轮次,每轮次ConcurrentAlloc%u次,花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次ConcurrentFree%u次,花费:%u ms\n",
nworks, rounds, ntimes, free_costtime);
printf("%u个线程并发执行ConcurrentAlloc&ConcurrentFree %u轮次,总花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime + free_costtime);
}
int main()
{
size_t n = 10000;
// 4个线程执行10轮每轮执行n次
cout << "==========================================" << endl;
BenchmarkMalloc(n, 4, 10);
cout << endl << endl;
BenchmarkConcurrentAlloc(n, 4, 10);
cout << "==========================================" << endl;
return 0;
}
%%性能优化:如果想要分析性能瓶颈,不能靠感觉,而是要使用性能分析的工具。
vs下,性能与诊断,性能导向,检测。%%
经过诊断后,发现centralcache
,pagecache
中桶锁和大锁的竞争消耗了大量的时间。还有在MapObjectSpan
建立映射的时候,unique_lock
会消耗大量的时间。
在tcmalloc
中是使用基数树来优化提高这个性能的,
是整数到整数的映射,使用基数树建立映射效果更好。
的映射
基数树其实就是哈希表的变形。
一层的基数树就是采用直接定址法的哈希表。
基数树需要传入分类型模板参数,BIIT表示存储页号需要的位数。
BITS = 32 - PAGE_SHIFT
。
BITS = 64 - PAGE_SHIFT
。
// 一层的基数树
template <int BLTS>
class TCMalloc_PageMap1
{
private:
// 32位下,一共有2^19个页号和span*映射
static const int LENGTH = 1 << BITS;
void** array_;
}
两层的基数树就是分层哈希
分层哈希
values
数组哈希,所以如果没有第一层的映射的话,第二层就不会开空间。三层的基数树也是同样的原理,只不过是将一个页号分成了三个部分,第一层映射了前x位,第二层映射了中间的y位,第三层映射了最后的z位。
// 两层的基数树
template <int BITS>
class TCMalloc_PageMap2
{
private:
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;
// 叶节点
struct Leaf
{
void* values[LEAF_LENGTH];
};
Leaf* root_[ROOT_LENGTH]; // 指向232个孩子
void* (*allocator)(size_t); // 内存分配
}
完整代码如下:
#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)) {
// array_ = reinterpret_cast((*allocator)(sizeof(void*) << BITS));
// memset(array_, 0, sizeof(void*) << BITS);
//}
explicit TCMalloc_PageMap1() {
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);
}
// 直接定址法
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)) {
// allocator_ = allocator;
// memset(root_, 0, sizeof(root_));
//}
explicit TCMalloc_PageMap2() {
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;
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;
}
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) {
static ObjPool<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() {
}
};
// 将页号和Span*建立一个映射
//std::unordered_map _idSpanMap;
// 使用TCMalloc_PageMap2对象代替unordered_map哈希表建立的映射
TCMalloc_PageMap2<32 - PAGE_SHIFT> _idSpanMap;
基数树的优点
unordered_map
底层的哈希表还是map
底层下的红黑树都会随着插入的过程中产生这个数据结构的变化。而因为整体结构的改变,那么当多个线程进入函数搜索的时候,可能会发生当一个线程因为在建立
的映射关系而导致整个树的结构产生改变,从而导致另一个正在寻找
的线程找不到原有的键值对。所以需要加锁ConcurrentFree
,那么是在合并内存ReleaseListToSpans
,这个时候其实基数树都是已经分配好结构的,所以可以直接读取。ReleaseSpanToPageCache
合并归还内存和NewSpan
开辟新的内存。**都是在pagecache
中进行的,而pagecache
都是有锁保护的。并且一块新开辟的内存也不可能会同时在释放。**所以也不会发现线程不安全的问题。其实就算是多个线程同时访问基数树的话,因为同一块内存不可能同时在进行创建和释放,所以也是没有必要对基数树进行加锁保护的。
因为基数树本身读写比哈希表更快,而且在使用MapObjectToSpan
的时候不用加锁了,所以使用整体性能上比原来更快。
CentralCache的作用
Span为什么设计成双链表?
为什么CentralCache中要使用Span来管理内存,而不是直接挂内存节点?
项目的完整代码存放