一.项目介绍
实现一个高并发的内存池,它的原型是Google的一个开源项目tcmalloc(Thread-Caching Malloc),即线程缓存的malloc,实现了高效的多线程内存管理,用于替代malloc和free的功能.
二.技术基础
需要了解线程的概念,需要会使用C/C++语言进行编程,要会使用stl中的容器(主要是哈希桶和链表结构),要熟悉操作系统内存管理的原理,要熟悉单例模式编程方法,要熟悉互斥锁的概念
三.池化技术
所谓池化技术,说的通俗一点,就是预先申请一批过量的资源,管理这些资源,放在那里等待被申请,特例化到我们这里的内存池,就是预先向OS申请一块足够大的空间,此后当程序需要申请内存时直接向内存池申请(从这个大空间上面切分).
内存池的好处就在于不用频繁的调用申请,我们知道每次调用malloc和free,它的内部肯定是封装了一些系统调用接口的,那么我们从用户态到内核态再从内核态到用户态这个过程,肯定会有一定的性能上面的消耗,如果我们预先申请了一批内存,那就减少了调用次数,也就大大提高了性能.(打个通俗的比方,就好比如和妈妈要生活费,每吃一顿饭就发个微信要十几块钱这种做法肯定是效率低下的,我们生活中往往采用的是妈妈一次给你很多生活费,你去吃饭时自己从钱包(也就是钱池)里面取);除此以外,内存池还能缓解内存碎片问题,我们知道申请的内存空间都是连续的,这也就代表着一些不连续的空间可能就会浪费掉:
例如上图中这种情况,,我们就无法申请出一个连续的16的内存空间.但如果我们的内存池支持吧这些内存碎片给拼接起来,那也就意味着可以缓解这里的内存碎片问题
内存碎片问题包括内碎片和外碎片:这里无法申请属于是外碎片问题,如果申请出的空间有部分不使用/浪费了,那就是内碎片问题.
四.定长内存池
思路:首先我们需要有一块大内存块memory来"切",然后还要有个freelist自由链表来管理还回来的内存对象,我们规定每次申请一块大内存为128kb那么就能得到下面成员变量
注意:
1.大内存块要用char* 类型来定义,这样便于一位一位的切割.
2._remainBytes是大块内存的最大空间(字节数),那我么需要思考一下:在什么情况下需要申请大块内存?----大块内存不够了-----不够来容纳所要申请的类型的大小了/不够容纳申请空间内存放一个指针变量了.(每个申请的空间都要有个指针变量以便释放回来时链接到freelist上)
内存申请思路:
所谓"申请内存"这个操作,其实际上就是去大内存块上面切分----大内存块空间足够给我们去切.
代码实现:
//如果要追求性能上的极致,我们可以用系统申请内存的接口来代替malloc(申请大内存块),因为malloc底层也是调用的系统接口,这样也能减少调用的步骤提高效率.
// 这样就能完全脱离malloc了
//----直接去堆上按页申请空间
#ifdef _WIN32
#include <Windows.h>
#endif //
#ifdef _WIN64
#include <Windows.h>
#endif // common.h已经有了这里就不要重复定义了
template<class T>
class ObjectPool
{
public:
T* New()
{
T* obj = nullptr;
//首先考虑在freelist上的内存对象
if(_freelist!=nullptr)
{
//把内存对象从链表上面取出
//头删
void* next = *(void**)_freelist;//记录freelist的新的头节点
obj = _freelist;
_freelist = next;
return obj;
}
else //freelist上为空就去找大内存块切分
{
//切分的前提是大内存块要有足够的内存供我们去切分
if(_remainBytes < sizeof(T)||_remainBytes < sizeof(void*))
{
_remainBytes = 1024*128;
//_memory = (char*)malloc(_remainBytes);
_memory = (char*)SystemAlloc(_remainBytes >> 13);
if(_memory == nullptr)
{
throw std::bad_alloc();
}
}
//空间够了就开始申请(切分)
obj = (T*)_memory;
//有一种可能就是obj类型的大小是比一个指针变量大小要小的,那这种情况
//就必须确保申请的空间能存储一个指针变量的大小
int objsize = sizeof(T)>sizeof(void*)?sizeof(T):sizeof(void*);
_memory += objsize;
_remainBytes -= objsize;
}
//定位new一下,显示调用T的构造函数初始化
new(obj)T;//我们只开了空间却没有初始化,所以需要这一步来初始化一下对象
return obj;
}
private:
char* _memory = nullptr;//提前申请的大块的内存,为什么用char?便于切割这个大内存块
size_t _remainBytes = 0;//大块内存剩余的空间(字节数)
void* _freeList = nullptr;//自由链表用来管理free(还回来)的内存块
};
注意:
1._memory再被切之前是需要确定它的大小(字节)是否足够容纳一个T对象/指针变量大小的,如果不够,就要申请
2.申请出的内存对象必须要能容纳一个指针变量,不然在释放的时候就无法连接上freelist了
内存释放思路:
思路:我们在释放内存时是不需要把这个内存对象给erase掉的,只需要把它挂到freelist上等待复用就好了.其实也就是"链表的头插"
代码实现:
template<class T>
class ObjectPool
{
public:
void Delete(T* obj)
{
//显示调用析构函数清理一下对象
obj->~T();
*(void**)obj = _freelist;
_freelist = obj;
}
private:
char* _memory = nullptr;//提前申请的大块的内存,为什么用char?便于切割这个大内存块
size_t _remainBytes = 0;//大块内存剩余的空间(字节数)
void* _freeList = nullptr;//自由链表用来管理free(还回来)的内存块
};
注意:
1.千万不要内存_memory给delete掉了,我们还留着要用呢!
2.内存池中的内存,只有在程序结束的时候会释放给操作系统.
测试代码
//这些类型在多个CPP中包含的话会发生一些预料之外的问题
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
,_left(nullptr)
,_right(nullptr)
{}
};
void TestObjectPoolOP()
{
//申请释放的轮次
const size_t Rounds = 3;
//每轮申请释放多少次
const size_t N = 100000;
size_t begin1 = clock();
std::vector<TreeNode*> v1;
v1.reserve(N);
for (size_t j = 0; j < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v1.push_back(new TreeNode);
}
for (int i = 0; i < N; i++)
{
delete v1[i];
}
v1.clear();
}
size_t end1 = clock();
ObjectPool<TreeNode> TNPool;//针对TreeNode类型设计的定长内存池
size_t begin2 = clock();
std::vector<TreeNode*> v2;
v2.reserve(N);
for (size_t j = 0; j < Rounds; ++j)
{
for (int i = 0; i < N; ++i)
{
v2.push_back(TNPool.New());
}
for (int i = 0; i < N; i++)
{
TNPool.Delete(v2[i]);
}
v2.clear();
}
size_t end2 = clock();
cout << "new cost time :" << end1 - begin1 << endl;
cout << "objectnew cost time :" << end2 - begin2 << endl;
}
总结:
定长内存池在申请定长的内存时,其效率是非常高的,但是由于它只能申请定长的内存,所以其使用还是受到了一定的限制,另外,对于内存碎片的问题,它也没有相应的应对措施.除此以外,在多线程并发的场景下,锁竞争太过于激烈,效率太低.
五.基本框架
高并发内存池可分为三层缓存:线程缓存,中心缓存,页缓存.
线程缓存Thread Cache:每个线程都独有一个内存池Thread Cache,这样就不用加锁了.
中心缓存Central Cache:当Thread Cache池子不够用了,就从这里拿,所有线程共享中心缓存,由于所有线程共享,所以中心缓存是要加锁的,事实上中心缓存的锁竞争并不是很激烈,只有当多个线程一起找同大小的内存时才会发生锁竞争.
页缓存PageCache:当CentralCache不够了,就会向页缓存申请.同时,当CentralCache上有太多空闲资源(Thread Cache还回来的)时,页缓存可以把这些资源按页的形式回收并合并以此来缓解内存碎片问题.当然页缓存也是所有线程共享的,所以也要加锁.
总结:内存池需要考虑的问题:1.内存碎片问题2.性能问题3.多线程情况下锁竞争问题(其实也就是锁竞争问题).
六.线程缓存Thread Cache
结构概念图:
Thread Cache是一个哈希桶结构,在哈希表映射着所要申请的内存大小,每个哈希桶指向对应大小的内存对象所组成的单链表Freelist.
进一步我们可以得出推论:Thread Cache其实就是成员为Freelist的数组(_freelists)
而Thread Cache上每个桶对应着所要申请的空间大小,我们不可能一个字节大小就对应一个桶,因此我们就必须让部分要申请的内存大小来根据对齐数对齐到一个桶上,而既然申请大小是对齐后的大小,那就势必会导致内存碎片问题,所以我们要谨慎选择对齐数:
这里我们以一次最大申请256KB大小的内存为例,可以制定这样的对齐规则:
// 控制在10%左右的内碎片浪费
// 申请内存范围 对齐规则 哈希桶的数量
// [1,128] 8byte对齐 freelist[0,16) 最多浪费7/8
// [128+1,1024] 16byte对齐 freelist[16,72) 最多浪费15/144
// [1024+1,8*1024] 128byte对齐 freelist[72,128) 最多浪费127/1152
// [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 size, size_t alignNum)
{
return ((size + alignNum - 1) & ~(alignNum - 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 <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
//这里就是>256kb的情况了
return _RoundUp(size, 1 << PAGE_SHIFT);
}
}
//置于common.h的class Sizeclass{};中.
同样的,既然我们知道了_freelist本质就是Freelist成员类型的数组,那我们还得找出所要申请的内存对象对应的Freelist在哪一个桶上(也就是我们通过对齐找到了真正要申请的内存大小,也就是桶映射的大小,但是在该桶在_freelist的下标我们是需要另外计算的)
static inline size_t _Index(size_t bytes, size_t align_shift)//align_shift就是"1左移了几位变成了对齐数"
{
//第几个freelist,例如:7对应0下标的freelist;9对应1下标的freelist;129到1024因为是下一个区间(桶)的,则会先-128,算出结果为0再加上16(前面所有的桶中的freelist总数)
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];//加上前面各个桶中已存在的freelist的数量
}
else if (bytes <= 8192)
{
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 65536)
{
return _Index(bytes - 8192, 10) + group_array[2] + group_array[1] + group_array[0];
}
else
{
assert(false);
}
return -1;
}
另外实现一下我们要用的内存对象的自由链表FreeList:
class FreeList
{
public:
void Push(void* obj)
{
assert(obj);
//头插
//*(void**)obj = _freeList;
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
void PushRange(void* start, void* end,size_t n)//插入一批内存空间
{
NextObj(end) = _freeList;//让end的尾插到他前面,那这个范围整体就完成了头插
_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 = 0;
};
我们将这些不在各个缓存区构造内的方法集中放到一个common.h中
模块职能
1.提供给用户申请空间(void* Allocate(size_t size))
2.提供给用户释放空间(void Deallocate(void* ptr, size_t size))
3.当对应大小的内存对象不足时,向Central Cache申请大块内存span以切分.
注意:在ThreaCache中还要保证每个线程私有一个内存池,我们这里使用的方法是:
每个线程都私有有一个pTLSThreadCache指针(Thread local storage)线程本地存储每个线程本地的ThreadCache指针,这样大部分情况是不需要锁的
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;这是一种特殊的内存机制,可以把数据存储在线程的私有内存中,这样线程就私有了数据存储空间.
代码实现Thread Cache:
ThreadCache.h
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);//这里的参数ptr是用来对应各个内存块的,而size就是用来找到对应大小的自由链表来将它挂起来
//从中心缓存获取对象
void* FetchFromCentralCache(size_t index,size_t alignSize);
//释放对象时,链表过长,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freelists[MAX_NFREELIST];//哈希表,每个位置挂的都是一个freelist,那这个数组是多大,就要看映射关系如何处理了.
};
//TLS thread local storage :在线程内是全局的,多个线程之间互相不干扰,这就不用加锁了.
//由于每个线程都私有一个thread cache,所以每个线程都应该有一个pTLSThreadCache指针
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
ThreadCache.cpp
Allocate:
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);//要申请的空间最大不会超过256kb,这是自己定的
//例如给的size在1到8的闭区间范围,就给8,在9到16的闭区间范围就给16----对齐(当然,在实际情况下不是这样子随便对齐的)
//我们可以设计一个类来管理映射规则
size_t alignSize = SizeClass::RoundUp(size);//得出对齐之后的申请内存的字节数.
size_t index = SizeClass::Index(size);//得出所要申请的内存在哪个freelist上
if (!_freelists[index].Empty())//根据当前桶上有没有内存块可以拿来用
{
//不为空就先用着
return _freelists[index].Pop();//对应的freelist弹出一个内存块
}
else
{
//freelists为空就从内存池上面切割(从中心缓存上面获取)
return FetchFromCentralCache(index,alignSize);
}
}
Deallocate:
void ThreadCache::Deallocate(void* ptr, size_t size)//要释放的内存,要释放的内存大小
{
assert(size <= MAX_BYTES);
assert(ptr);
size_t index = SizeClass::Index(size);//找到对应桶,_freelist的下标
_freelists[index].Push(ptr);//找出映射的自由链表,插入进去
//但是随着不断地Deallocate,freelist的长度太长了,就会导致很多内存对象挂在这里浪费
//我们简化一下:如果freelist现在的长度大小超过了一次批量申请的最大长度,那就认为它过长了.这时就还一部分内存对象给central cache
if (_freelists[index].Size() >= _freelists[index].MaxSize())
{
ListTooLong(_freelists[index], size);
}
}
ListTooLong:
随着不断地Deallocate释放内存对象,ThreadCache上的空闲内存对象会越来越多,当达到一定的程度,就会向上还给CentralCache.这里在Freelist上Pop的就不是一个单一的内存对象了,就是一批内存对象了.
而向上传给Central Cache的方法ReleaseListToSpans会关系到Central Cache的结构,我们就放到Central Cache中去实现.
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.MaxSize());//从当前freeList中取出可批量申请的最大长度的数量的内存对象
//将这些内存对象还给central cache
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
FetchFromCentralCache
当ThreadCache上对应桶为空而没有内存对象可以供申请了.就可以去找Central Cache申请一个span并将其切分成多个内存对象构成一个Freelist挂在对应的桶上.
在这里我们需要注意的是,我们到底要多少个内存对象来挂在桶上呢?首先,"申请不止一个内存对象",这是肯定的(池化思想),但到底要申请几个,这可不能随便定(多了会浪费,少了拖效率),为了达到更好的性能,对于偏小的情况,我们可以适当多要一点,而对于一些比较大的内存对象缺少的情况,我们可以则少要一点.
static size_t NumMoveSize(size_t size)
{
/*if (size == 0)
return 0;*/
assert(size > 0);
//[2,512],一次批量移动多少个对象的(慢启动)上限值
//小对象就多给点,大对象就少给点
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
这里我们采取的方法是慢开始反馈调节算法:小的多给点,大的少给点
最开始不会一次向central cache要太多内存对象,因为要太多的话也有可能会用不完
但是如果不断的有size大小的内存的需求,那么batchNum就会不断增长.直到上限.
size越大,一次向central cache要的batchNum就越小.
size越小, 一次向central cache要的batchNum就越大(不断增长的"大").
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//向central cache申请一批内存块插到对应的thread cache的对应的freelist上:
// 1.先根据所要申请的这批内存块的大小算出实际真正要去申请的内存块大小(对齐)
// 2.根据要申请的内存块的大小找到对应的FreeList
// 上面两步在Alloc步骤已经完成,
// 3.根据实际要申请的内存块的大小,用特定规则计算出实际要申请的内存块的数量.
//慢开始反馈调节算法:小的多给点,大的少给点
//最开始不会一次向central cache要太多内存对象,因为要太多的话也有可能会用不完
//但是如果不断的有size大小的内存的需求,那么batchNum就会不断增长.直到上限.
//size越大,一次向central cache要的batchNum就越小.
//size越小, 一次向central cache要的batchNum就越大(不断增长的"大").
size_t batchNum = min(_freelists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (_freelists[index].MaxSize() == batchNum)//即如果MaxSize小于NumMoveSize,就增长一次MaxSize
{
_freelists[index].MaxSize() += 1;//MaxSize的初始值是1,如果一直有相同大小的内存块请求申请,MaxSize就会慢增长,慢慢增长到512
}
//开始向central cache申请内存对象
//输出型参数,会把申请到的内存地址交给start和end
void* start = nullptr;
void* end = nullptr;
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);//有可能不足batchNum个,此时就能拿多少拿多少
assert(actualNum > 0);//至少申请了一个
if (actualNum == 1)
{
assert(start == end);
return start;
}
else
{//如果申请了很多内存对象,那就可以把这些内存对象头插在thread cache的对应freelist上
_freelists[index].PushRange(NextObj(start), end,actualNum-1);//因为要返回,所以可以传NextObj()的start.
return start;
}
return nullptr;
}
七.中心缓存Central Cache
结构概念图:
CentralCache也是个哈希结构,但是每个哈希桶上指向的是一个个大内存块组成的双链表,每个大内存块可切分为桶对应的内存对象大小.而我们ThreadCache上的内存对象不足时,就会到对应的桶上找一个非空的Span来切分,当然,如果一个桶中所有的Span对象都被切空了,那CentralCache就再得向上一层PageCache去申请Span对象.除此以外,我们前面也提到过,如果CentralCache上太多闲置的Span(也就是没有被切分的),那就会被PageCache回收,以此缓解内存碎片问题
Span对象到底是个啥呢?
我们可以实现出来
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;//是否在被使用
};
Span其实就是许多页的内存对象
(1)这里的pageid的PAGE_ID 类型是什么呢?
在32位下,内存地址空间(mm_struct*)有4GB(用户3GB,内核1GB)也就是2^32Byte,而一页内存是2^13Byte,因此总共有2^19页,这时用size_t 是能吧所有页号囊括的;但是如果在64位下,那内存地址空间就总共有2^64Byte大小,总共就有2^51页,这时size_t就不够了(size_t的取值范围就是unsigned int的取值范围:2^32-1),因为是需要用条件编译来分情况选择PAGE_ID的类型的
#ifdef _WIN32
typedef size_t PAGE_ID
#elif _WIN64
typedef unsigned long long PAGE_ID
#endif
这个写法是错误的!因为_WIN64中是包含了_WIN32的,所以如果按照上面这个写法进行条件编译,那就会导致64位系统下还是size_t定义的PAGE_ID,解决方式也很简单,将这两个条件顺序反过来就行了 :
#ifdef _WIN64
typedef unsigned long long PAGE_ID
#elif _WIN32
typedef size_t PAGE_ID
#endif
(2)_usecount就是该Span被使用的次数,当这个变量的值为0时,说明这个Span是空闲的,就需要把这个Span释放给PageCache.每当有ThreadCache::FetchFromCentralCache(也就是CentralCache::FetchRangeObj)的调用,那被切分走内存对象的Span的_usecount就会++,每当有ThreadCache::Deallocate(其中的ListTooLong)(也就是CentralCache::GetInstance()->ReleaseListToSpans)被调用,那么释放内存对象链接上的Span的_usecount就会--.
(3)而_isUse则是用来释放Span给PageCache时用到的成员,简单说一下,其实不难想象,PageCache回收Span对象,其实就是看它的_usecount是否为0,是的话就回收,但也有一种可能,就是CentralCache刚从PageCache中申请的Span对象屁股还没坐稳,你就说它_usecount为0,就要回收人家.为了避免这种乌龙,我们的_isUse布尔值就用来标识这个Span是否是正在使用的,如果是就不要回收它.
另外,同样的既然知道每个桶指向的是个Span组成的双链表,那我们就可以实现出这个SpanList类
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 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;
//delete pos;注意这里是不需要delete掉的,而是要还给page cache的!!!
}
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;
}
private:
Span* _head;//头节点
public:
std::mutex _mtx;//桶锁,有多少个spanlist,他就有多少把锁
};
模块职能
相较于ThreadCache直接面向用户,CentralCache处于我们的框架中间一层,是所有线程共享的,这就意味着它只能有一个实例,这就需要用到单例模式.
而它的功能就包括:
1.ThreadCache申请内存对象时,获取一定数量的内存对象给Thread Cache
2.ThreadCache有过多闲置内存对象时,释放一部分内存对象给CentralCache对应桶上的Span
3.CentralCache某个桶上没有非空Span可供切分给ThreadCache时,向PageCache申请一个Span对象
代码实现Central Cache:
CentralCache.h
#pragma once
#include "Common.h"
//1.它的映射规则和treadcache一样
//2.桶锁
class CentralCache
{
//单例模式
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
//获取一个非空的Span
Span* GetOneSpan(SpanList& list, size_t size);
//从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t byte_size);
//将一定数量的内存对象释放还给span
void ReleaseListToSpans(void* start, size_t byte_size);
private:
SpanList _spanlists[MAX_NFREELIST];
private:
CentralCache()//封死构造函数
{}
CentralCache(const CentralCache&) = delete;//封死拷贝构造函数
static CentralCache _sInst;//饿汉模式,上来直接定义对象
};
其中GetOneSpan方法就是调用FetchRangeObj内部的方法,如果当前桶上有非空的Span对象,那就从这个Span上面切分,如果没有那就去PageCache上申请.
CentraCache.cpp
FetchRangeObj
在对应的桶上找到一个Span,然后对这个Span切分的方法就类似于我们定长内存池中切分_memory的方法(先让start end指向开头,然后end向后走,最后让开头指针指像Nexobj(end),并且把Nextobj(end)置为空就完成了),但是这里需要注意的是,由于CentralCache是所有线程共享的,所以在做任何修改它的操作时都要加锁,我们这里加的就是"桶锁",因为别的桶的SpanList不一定需要被切分.
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);//先算出在哪个桶上(映射规则和thread cache一样)
_spanlists[index]._mtx.lock();
Span* span = GetOneSpan(_spanlists[index],size);
assert(span);
assert(span->_freeList);//非空
//要从span上切割内存块下来,可以先让end向后走batchNum步,之后再让_freelist指向end的后面一个内存块
start = span->_freeList;
end = start;
size_t i = 0;//切分次数,切0次就直接拿走1个,切一次就拿走两个
size_t actualNum = 1;//既然span不为空,则必至少有一个小内存对象
while(i < batchNum-1&&NextObj(end)!=nullptr)//i是end向后移动的次数,当end不动,那就会取一块,end向后移动1次就会取两块,即i+1的值就是最后申请的内存对象的个数
{
end = NextObj(end);
++i;
++actualNum;
}
span->_freeList = NextObj(end);//_freelist上的内存块被拿走了
NextObj(end) = nullptr;//取走的内存块最后一个内存块指向空
span->_usecount += actualNum;
_spanlists[index]._mtx.unlock();
return actualNum;
}
ReleaseListToSpans
当ThreadCache某个桶中的内存对象过多,就会释放回CentralCache
具体操作为:现根据内存对象大小找到在对应哪一个桶上,再根据内存对象自由链表的地址来映射出是在哪一个Span上,再一个一个头插到对应的Span对象下.
当CentralCache中有span的内存对象全部都被释放回来了(_usecount==0),那就会继续向上层释放给PageCache.
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->_freeList = start;
span->_usecount--;
if (span->_usecount == 0)
{
//说明span的所有内存对象都还回来了,此时就要把span还给pagecache,pagecache再尝试做前后页的合并
_spanlists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
//释放span给pagecache时,使用pagecache的锁就行,暂时可以吧桶锁给解开
_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();
}
这里的MapObjectToSpan其实就是获取从内存对象到其对应的Span的映射,因为同一个Span下的内存对象的页号是相同的,而Span的页号又是由它的地址决定,所以一定范围的地址就对应同一个页号,通过这层映射来帮助小内存对象找到Span对象(在PageCache层中实现)
GetOneSpan
为了方便ThreadCache申请内存对象FetchRangeObj中有了GetOneSpan来找到一个非空Span切分内存对象给ThreadCache.
毫无疑问的是先从当前桶中遍历找到一个非空Span,找到了就直接用.但是如果当前桶中一个非空Span都没用,那就只能去找PageCache要了.
在和PageCache申请span时也要注意PageCache也是所有线程共享的,所以也必须得加锁
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//先看下一当前SpanList中有没有非空的了,如果没有就只能向page cache要
//当前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();
//page cache是整个加锁,不能是桶锁
PageCache::GetInstance()->_pageMtx.lock();
//向page cache要
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//NumMovePage可以根据size大小知道要申请的span的页数
span->_isUse = true;
span->_objsize = size;//把要切的大小给记录下来,以后后面free的时候使用
PageCache::GetInstance()->_pageMtx.unlock();
//把要到的大Span进行切分(注意,这里不需要加锁,因为其他线程是拿不到这个span的)
//要想切割span,必须要先知道span的起始地址----就是页号>>PAGE_SHIFT(通俗来讲,页号就是第几页--第几个8KB==页号*2^13即页号<<PAGE_SHIFT(13))
char* spanaddr = (char*)(span->_pageid << PAGE_SHIFT);//用char*可以用+bytes的操作
size_t bytes = span->_n << PAGE_SHIFT;//计算出这个span的内存大小
void* end = spanaddr + bytes;
//先切一块下来作为头,然后尾插
span->_freeList = spanaddr;
spanaddr += size;
void* tail = span->_freeList;
while (spanaddr < end)
{
NextObj(tail) = spanaddr;
tail = spanaddr;
spanaddr += size;
}
NextObj(tail) = nullptr;//tail指向为空
//此时span已经被切成了很多段了
//切好span后,在吧span挂到桶上的时候再加锁
list._mtx.lock();
list.PushFront(span);
return span;
}
八.页缓存
结构概念图:
PageCache依然是一个哈希结构,但是它却与前面两个有着区别,他的映射关系是一页一页谁也不漏的对应的,页数为多少的span就挂在标识多少页的桶下.当CentralCache向它申请Span对象时,他会根据要申请的Span页数去PageCache对应的桶上找有没有Span对象可供申请,如果当前桶上没有,那就向后直到128page桶上面找有没有Span对象,如果有就拿一个后面的大Span对象切分,一份为我们要申请的Span,另一份就是被切剩下的,把这个剩下的Span对象插入到对应的PageCache的桶上.
如果找到_spanList[128]都没有合适的span,则PageCache就会向堆申请一个128页的大Span挂在最后一个桶上,然后重复前面的过程(这时就会把这个新申请的大Span给切分)
完成申请后,要记得切分一下CentralCache申请的那个Span对象
当有span从CentralCache上释放过来后,需要判断这个span的前后pageid对应的span是不是空闲的,如果是,那就把它们合成一个大Span一起传过来,以此缓解内存碎片问题.
模块职能
PageCache是所有线程共享的,因此得用单例模式
1.CentralCache申请span内存对象时,PageCache需要搞一个Span对象给它
2.CentralCache释放资源给PageCache
代码实现Page Cache :
PageCache.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];//128个桶
ObjectPool<Span> _SpanPool;
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
//根据定理:同一页下的所有切割下来的内存对象的页号都相等,可以根据span和页号的映射关系建立unordered_map
std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};
在这其中,_idSpanMap则是建立出地址和页号的映射关系
:页号就是第几页 == 第几个2^13--->地址就等于页号乘每页大小 == _pageid*2^13 === _pageid>>13
PageCache.cpp
NewSpan
思路 :
当CentralCache向它申请Span对象时,他会根据要申请的Span页数去PageCache对应的桶上找有没有Span对象可供申请,如果当前桶上没有,那就向后直到128page桶上面找有没有Span对象,如果有就拿一个后面的大Span对象切分,一份为我们要申请的Span,另一份就是被切剩下的,把这个剩下的Span对象插入到对应的PageCache的桶上.
如果找到_spanList[128]都没有合适的span,则PageCache就会向堆申请一个128页的大Span挂在最后一个桶上,然后重复前面的过程(这时就会把这个新申请的大Span给切分)
Span* PageCache::NewSpan(size_t k)
{
//先看当前页桶里面有没有对应的span,没有就往后搜索,找到了就切,找不到就去跟OS要
assert(k > 0);
//大于128页的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;可以使用定长内存池来代替new,彻底与new脱离关系,这样性能就能再上升一层
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,直接取
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;
}
else
{
//检查一下后面的桶里面有没有span,如果有可以把它进行切分
for (size_t i = k + 1; i < NPAGES; i++)
{
if (!_spanLists[i].Empty())
{
//切 k页的span给central cache,其余的span挂给对应的桶
Span* nSpan = _spanLists[i].PopFront();
/*Span* kSpan = new Span;*/
Span* kSpan = _SpanPool.New();
//在nSpan的头部切一个k页下来
kSpan->_pageid = nSpan->_pageid;
kSpan->_n = k;
nSpan->_pageid += k;
nSpan->_n -= k;
//把切完的nSpan挂到对应位置
_spanLists[nSpan->_n].PushFront(nSpan);
//存储nSpan(其实此时已经是(n-k)Span了)的首尾页号跟nSpan映射,方便page cache回收内存时进行的合并查找
_idSpanMap[nSpan->_pageid] = nSpan;
_idSpanMap[nSpan->_pageid + nSpan->_n-1] = nSpan;
//在吧span分配给central cache时最好记录下它们的_pageid
//建立id和span的映射,方便central cache回收小块内存对象时查找对应的span
for(PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageid + i] = kSpan;
}
return kSpan;
}
}
//找完了128个桶都没找到(pagecache最开始就是处于这样的状态)
//走到这里就说明后面没有大页的span了,这时就要去找堆要一个128页的span
/*Span* bigSpan = new Span;*/
Span* bigSpan = _SpanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_n = NPAGES - 1;
bigSpan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;//找堆要的时候也会按对齐的方式给,所以直接除以8kb就能得到页号了
_spanLists[bigSpan->_n].PushFront(bigSpan);//把bigspan插到128页的桶中
return NewSpan(k);//再调用一次,此时128页的桶中必有span了,直接对这个span切分即可.
}
}
MapObjectToSpan
//获取从对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
//根据内存对象的地址找出span页号
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;//(右移13位)
std::unique_lock<std::mutex> lock(_pageMtx);//RAII的锁,对象销毁自动解锁
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())//找到了(unordered_map如果找不到就会返回end())
{
return ret->second;//span的指针
}
else
{
//不可能找不到的,_spanLists
assert(false);
return nullptr;
}
}
ReleaseSpanToPageCache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于128页的直接还给堆
if (span->_n > NPAGES - 1)
{
//找堆要的
void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
SystemFree(ptr);
//delete span;//span是new出来的,需要delete掉
_SpanPool.Delete(span);
return;
}
// 对span前后的页,尝试进行合并,缓解内存碎片问题
//向前合并
while (1)
{
PAGE_ID previd = span->_pageid - 1;
auto ret = _idSpanMap.find(previd);
if (ret != _idSpanMap.end())
{
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{//在central Cache中使用,就不合并了
break;
}
if (prevSpan->_n + span->_n > NPAGES - 1)
{
//合并出了超过了128页的span了,此时没有桶来装他,无法管理,就不合并
break;
}
//开始合并
span->_pageid = prevSpan->_pageid;
span->_n += prevSpan->_n;
//prevspan本来是在page cache上的,需要把它从对应桶上面给删了
_spanLists[prevSpan->_n].Erase(prevSpan);
/*delete prevSpan;*/
_SpanPool.Delete(prevSpan);
}
else//前面的页号不存在,不进行合并(可能系统没分给我 因为virtualAlloc分哪些空间给我们是系统决定的)
break;
}
//向后合并
while (1)
{
PAGE_ID nextid = span->_pageid + span->_n;//和prev不一样的是,下一页的页号必须是从span的页号开始向后走过页数才能找到
auto ret = _idSpanMap.find(nextid);
if (ret != _idSpanMap.end())
{
Span* nextspan = ret->second;
if (nextspan->_isUse == true)
break;
if (nextspan->_n + span->_n > NPAGES - 1)
break;
//开始合并
//页号还是原来的,这次是从尾部合并
span->_n += nextspan->_n;
//nextspan本来是在page cache上的,需要把它从对应桶上面给删了
_spanLists[nextspan->_n].Erase(nextspan);
/*delete nextspan;*/
_SpanPool.Delete(nextspan);
}
else
break;
}
//向前向后都合并过了,现在就需要把这个span给挂起来
_spanLists[span->_n].PushFront(span);
span->_isUse = false;
_idSpanMap[span->_pageid] = span;
_idSpanMap[span->_pageid + span->_n - 1] = span;
}
这样一来底层的实现就到这里就差不多完成了.
再给一些面向用户的接口就能初步完成整个进程池的设计了
详细代码实现:项目: 在研究高并发内存池项目时所用的练习代码 (gitee.com)
(v7)
九.性能提升
通过VS下的性能诊断工具,发现互斥锁对于性能还是存在较大的影响.
我去对Google下的tcmalloc源码进行观察,发现他们是采用基数树来解决这个问题的
#pragma once
#include"Common.release.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() {
}
};
这样一来就能减少互斥锁的性能消耗了.(基数树在写之前会提前开好空间,不会修改结构,因此不用加锁)
#include "ConcurrentAlloc.release.h"
//ntimes 一轮申请和释放内存的次数
//rounds 轮次
//nworks 线程数
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
size_t malloc_costtime = 0;
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",
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);
}
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
size_t malloc_costtime = 0;
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",
nworks, rounds, ntimes, malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%ums\n",
nworks, rounds, ntimes, free_costtime);
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime + free_costtime);
}
int main()
{
cout << "==========================================================" << endl;
BenchmarkMalloc(10000, 4, 10);
cout << endl << endl;
BenchmarkConcurrentMalloc(10000, 4, 10);
cout << "==========================================================" << endl;
return 0;
}
我们用如上的代码测试了我们高并发内存池的性能,最终发现它在多线程情况下性能确实是要优于malloc和free的.