目录
高并发内存池
一、项目介绍
二、项目涉及知识和项目环境
三、定长内存池
四、整体框架:
五、ConcurrentAlloc.h
六、ThreadCaChe
七、CentralCache:
八、PageCache:
九、测试以及性能分析:
十、优化方向和优化方法:
十一、结束语
1、当前项目是实现一个高并发的内存池,原型是google的一个开源项目tcmalloc,tcmalloc全称
Thread-Caching Malloc,即线程缓存的malloc,实现了实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。
2、tcmalloc的原型项目太大了,细节太多,当前项目的目的是把tcmalloc的框架抽取出来,简化细节实现一个高并发的内存池,并在多线程的情况下和malloc做对比,实现在多线程环境下,申请释放对象比malloc更加高效。
1、知识:C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁
等等方面。
2、环境:VS2019。
1、池化技术:就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。
2、那我们就可以为一个类型的对象创建一个内存池,自己管理,那这样就减少大量的向系统申请释放内存的消耗,从而提高效率。
3、设计思路:
(1)、一次性向内存申请一大块内存,当前项目是16页(一页8192字节),然后用一个指针变量指向内存起始地址,是char*类型的_memory;还有实时记录剩余字节数:size_t类型的_remainBytes,接下来就是管理释放回来的小内存块,当前项目使用内存的前4/8(根据系统位数)个字节存储下一个小内存块的地址,所以当内存小于4/8字节时,也会分配出去4/8字节,这就是内碎片;然后用void*类型_freeList变量指向第一块内存小块,这样就能将所有的内存块链接起来,形成自由链表。自由链表下有空闲的内存块,优先使用。
(2)、当前项目是为了和malloc对比,那自然申请内存要脱离malloc(new底层也是封装了malloc),所以当前项目实现windows环境下的系统调用接口:VirtualAlloc和VirtualFree。(Linux使用brk、sbrk以及mmap、ummap,铁子们可以自行查阅,感兴趣也可以搜tpmalloc)
VirtualAlloc和VirtualFree函数原型就不介绍了:直接上代码:
#ifdef _WIN32
#include
#else
// Linux
// #include --brk
// #include --mmap
#endif
// 规定一页为8K
// 直接像系统申请内存,按页申请
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
// MEM_COMMIT | MEM_RESERVE 提交和保留
// PAGE_READWRITE:此区域读写
void* ptr = VirtualAlloc(nullptr, kpage << PAGE_SHIFT, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 注意:未使用MEM_RESET字段,此函数会默认初始化0
#else
// brk // sbrk
// mmap
#endif
if (ptr == nullptr)
{
// 失败抛异常
throw std::bad_alloc();
}
return ptr;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
// MEM_RELEASE 释放指定页,如果指定了这个类型,则dwSize应设置为0, 否则函数会调用失败
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// brk // sbrk
// unmmap
#endif
定长对象池类:
// 定长对象池
template
class ObjectPool
{
public:
// 申请内存
T* New()
{
T* obj = nullptr;
// 优先使用已经使用过的
if (_freeList != nullptr)
{
obj= (T*)_freeList;
_freeList = *(void**)_freeList;
}
else
{
// 因为管理时,需要前面4/8个字节来存储地址,所以要不小于4/8个字节
size_t objSize = sizeof(T) > sizeof(void*) ? sizeof(T) : sizeof(void*);
// 如果剩余空间不够申请
if (_remainBytes < objSize)
{
// 申请16页
_remainBytes = 16 << PAGE_SHIFT;
_memory = (char*)SystemAlloc(_remainBytes >> PAGE_SHIFT);
}
// 起始地址也出去
obj = (T*)_memory;
// 分出去objSize
_remainBytes -= objSize;
_memory += objSize;
}
// 调用定位new初始化对象
new(obj)T;
return obj;
}
void Free(T* ptr)
{
// 显示调用析构
ptr->~T();
// 头插自由链表
*(void**)ptr = _freeList;
_freeList = ptr;
}
private:
char* _memory = nullptr; // 指向剩余内存的起始地址
size_t _remainBytes = 0; // 剩余空间内存字节数
void* _freeList = nullptr;// 管理释放回来的第一个对象的地址,对象前4/8个字节存下一个对象的地址,形成链表
};
1、ThreadCache对象每个线程私有一个,CentralCache和PageCache对象全局只有一个,使用单例模式饿汉模式设计。
2、性能问题:多线程程序的性能问题突出体现在访问临界资源时的线程互斥问,也就是锁竞争问题,当前项目的框架:访问ThreadCache是不用加锁的,而访问CentralCache会加上桶锁,同时申请相同的内存的概率降低,锁竞争减少;访问PageCache因为涉及切割大页,合并前后页,所以只要访问PageCache就需要加锁。整体锁竞争是减少的。
3、内存碎片问题:
内碎片:当前项目PageCache可以接受申请不大于256KB的内存大小申请;如果大于256KB小于1M就直接找PageCache申请,大于1M直接找系统堆申请。
所以ThreadCache和Central为了把小块内存管理起来,不可能小块内存的间隔是1字节,需要存在内存对齐的规则,减少管理成本,当前项目把内碎片控制在10%左右;对齐规则如下:
外碎片:当ThreadCache下某个自由链表下挂的内存太多,就归还一部分给CentralCache,CentralCache再把空闲的Span对象下的自由链表内存归还给PageCache,由PageCache进行前后页合并,形成大内存页,缓解外碎片问题。
(1)、这个头文件就是提供ConcurrentAlloc和ConcurrentFree方法给上层调用。
(2)、ConcurrentAlloc:大于256KB直接走PageCache的申请流程;小于走ThreadCache的流程。第一次申请要动态开辟一个ThreadCache(在ThreadCache里面声明)对象。
ConcurrentFree:大于256KB直接走PageCache的释放流程。
#pragma once
#include "ThreadCache.h"
#include "ObjectPool.h"
#include "PageCache.h"
// 申请
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
// 对齐, 大于256KB按1<> PAGE_SHIFT;
// 加锁
PageCache::GetInstance()->_pageMutex.lock();
// 申请
Span* span = PageCache::GetInstance()->NewSpan(kPage);
span->_objSize = size;
PageCache::GetInstance()->_pageMutex.unlock();
// 返回
return (void*)((span->_pageId) << PAGE_SHIFT);
}
else
{
if (pTLSThreadCache == nullptr)
{
// pThreadCache = new ThreadCache;
// 用对象池创建,脱离malloc
static ObjectPool tcPool;
pTLSThreadCache = tcPool.New();
}
// 测试TLS
// cout << std::this_thread::get_id() << ": " << pTLSThreadCache << endl;
return pTLSThreadCache->Allocate(size);
}
}
// 释放
static void ConcurrentFree(void* ptr)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
// 大于MAX_BYTES直接走PageCache
if (size > MAX_BYTES)
{
PageCache::GetInstance()->_pageMutex.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMutex.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
(1)、ThreadCache的结构就是按照对齐规则208个FreeList结构,FreeList的_freeList指向对应字节数的内存块,形成哈希桶结构。
类定义源码:
#pragma once
#include "Common.h"
class ThreadCache
{
public:
// 申请
void* Allocate(size_t size);
// 释放
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
// 自由链表挂着个数太多,归还CentralCache
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeList[NFREELISTS]; // ThreadCache的结构是一个哈希映射的对象自由链表构成
};
// 线程私有变量创建,其他线程看不到此变量
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
(2)、每个线程各自私有一个ThreadCache的对象;
windows:静态:_declspec(thread) 类型 data=初始值;
Linux:静态:__thread 类型 data;
动态需要都需要几个接口进行维护,感兴趣搜素一下就有了。
以下是一些全局常变量的定义,FreeList(_maxSize和_size用来统计个数还内存):
// 小于等于MAX_BYTES找ThreadCache申请,大于找PageCache或者系统堆
static const size_t MAX_BYTES = 256 * 1024;
// 一个页默认8K,位移动13位
static const size_t PAGE_SHIFT = 13;
// ThreadCache和CentralCache哈希桶表的大小
static const size_t NFREELISTS = 208;
// PageCache哈希桶表的大小[0-128]
static const size_t NPAGES = 129;
// 获取对象内存的前4/8个字节
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
class FreeList
{
public:
void PushFront(void* obj)
{
assert(obj);
// 头插
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
// 插入一段区间
void PushRange(void* start, void* end, size_t n)
{
assert(start && end);
NextObj(end) = _freeList;
_freeList = start;
_size += n;
}
void* PopFront()
{
assert(_freeList);
// 头删,弹出一个对象内存
void* obj = _freeList;
_freeList = NextObj(_freeList);
--_size;
return obj;
}
// 弹出一段区间,不够把全部弹出
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;
}
// 判断是否为空
bool Empty()
{
return _freeList == nullptr;
}
// 最大个值由慢启动处修改,返回引用
size_t& MaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList = nullptr; // 自由链表桶下第一个块内存的地址
size_t _maxSize = 1; // 桶下最多能存多少个对象内存,多了要还给CentralCache,根据慢启动变化
size_t _size = 0; // 实时个数
};
(3)、在编码时,需要根据对象的大小和对齐规则确认此次应该分配多少字节,以及该大小的内存块应该去ThreadCache的哪号桶找空闲的内存块;所以设计了一个SizeClass提供各种静态的方法(还有根据对象大小,CentralCache决定一次性给ThreadCache多少个对象和根据CentralCache一次给ThreadCache大小,CentralCache向PageCache申请多少页):
注意:当前项目特别强调性能,所以谷歌的大佬,把一些if判断总结成位运算。
//计算对象大小的对齐映射规则
class SizeClass
{
public:
// 对象大小越大,对齐数越大,浪费的字节数越多,但是浪费率基本都在百分之11
// 这样设计是为了减少每个缓存链表桶表中链表桶的个数
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [128+1,1024] 16byte对齐 freelist[16,72)
// [1024+1,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
// 大于256K 按页对齐
// 根据对齐数和对象大小计算实际应该分配给对象多少字节 比如:5 8 13 16
//static inline size_t _RoundUp(size_t btypes, size_t alignNum)
//{
// if (btypes % alignNum != 0)
// {
// //向上取整
// return (btypes / alignNum + 1) * alignNum;
// }
// else
// {
// return btypes;
// }
//}
// (btypes + alignNum - 1)处理8 16 24这边界
// ~(alignNum - 1)把低位都处理为0,取8的整数倍
static inline size_t _RoundUp(size_t btypes, size_t alignNum)
{
return (btypes + alignNum - 1) & ~(alignNum - 1);
}
static inline size_t RoundUp(size_t btypes)
{
if (btypes <= 128)
{
return _RoundUp(btypes, 8);
}
else if (btypes <= 1024)
{
return _RoundUp(btypes, 16);
}
else if (btypes <= 8 * 1024)
{
return _RoundUp(btypes, 128);
}
else if (btypes <= 64 * 1024)
{
return _RoundUp(btypes, 1024);
}
else if (btypes <= 256 * 1024)
{
return _RoundUp(btypes, 8 * 1024);
}
else
{
return _RoundUp(btypes, 1 << PAGE_SHIFT);
}
}
// 算出当前对象大小在对应区间的哪号下标(相对值)
// 用对齐数算
/*static inline size_t _Index(size_t btypes, size_t alignNum)
{
if (btypes % alignNum == 0)
{
return btypes / alignNum - 1;
}
else
{
return btypes / alignNum;
}
}*/
// 用对齐数的指数算
static inline size_t _Index(size_t btypes, size_t alignShift)
{
return ((btypes + (1 << alignShift) - 1) >> alignShift) - 1;
}
static inline size_t Index(size_t btypes)
{
assert(btypes <= MAX_BYTES);
//每个区间链表桶的个数,最后一个区间不需要显示出来
//为了给算出下标加上基准
static int group_array[4] = { 16, 56, 56,56 };
if (btypes <= 128)
{
return _Index(btypes, 3);
}
else if (btypes <= 1024)
{
return _Index(btypes - 128, 4) + group_array[0];
}
else if (btypes <= 8 * 1024)
{
return _Index(btypes - 1024, 7) + group_array[0] + group_array[1];
}
else if (btypes <= 64 * 1024)
{
return _Index(btypes - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
else if (btypes <= 256 * 1024)
{
return _Index(btypes - 64 * 1024, 13) + group_array[0] + group_array[1] + group_array[2] + group_array[3];
}
else
{
assert(false);
return -1;
}
}
// 根据对象大小,CentralCache决定一次性给ThreadCache多少个对象
// 此数量是慢启动的上限值
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
//[2, 512],上限值在这个区间内
//小对象分的多,大对象分的少
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
else if (num > 512)
num = 512;
return num;
}
//根据CentralCache一次给ThreadCache大小,CentralCache向PageCache申请多少页
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
// 一次给的字节数
size_t npage = num * size;
//按页给
npage >>= PAGE_SHIFT;
if(npage == 0)
npage = 1;
return npage;
}
};
(4)、申请释放流程:
源码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include "ThreadCache.h"
#include "CentralCache.h"
// 申请
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
// 获得对齐后的字节数
size_t alignSize = SizeClass::RoundUp(size);
// 找到对应自由链表的下标
size_t index = SizeClass::Index(size);
// 判断自由链表有没有挂着桶
if (_freeList[index].Empty())
// 没有得去找中心缓存要
return FetchFromCentralCache(index, alignSize);
else
return _freeList[index].PopFront();
}
// 从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// 慢启动,根据ThreadCache申请的次数和申请的对象大小慢慢增加
// 一开始不给太多,_freeList里面_maxSize一起控制,类似TCP滑动窗口的慢启动算法
// 设置上下限:SizeClass设置
// 根据对象大小控制
size_t bacthNum = min(SizeClass::NumMoveSize(size), _freeList[index].MaxSize());
if (bacthNum == _freeList[index].MaxSize())
{
_freeList[index].MaxSize() += 1;
}
// 获取batchNum个对象
void* start = nullptr;
void* end = nullptr;
// 获取
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, bacthNum, size);
assert(actualNum >= 1);
// 只有一个
if (start == end)
return start;
// 有n个,留出一个,其他挂在链表下
else
{
_freeList[index].PushRange(NextObj(start), end, actualNum - 1);
NextObj(start) = nullptr;
return start;
}
}
// 释放
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
size_t index = SizeClass::Index(size);
// 头插到对应链表下
_freeList[index].PushFront(ptr);
if (_freeList[index].Size() >= _freeList[index].MaxSize())
{
ListTooLong(_freeList[index], size);
}
}
// 自由链表挂着个数太多,归还CentralCache
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.MaxSize());
// 归还中心缓存
CentralCache::GetInstance()->ReleaseListToSpan(start, size);
}
(1)、CentralCache的结构就是208个SpanList的双向带头循环链表构成的,对象大小和下标的映射规则和ThreadCache完全一致,因为CentralCache向PageCache申请内存是按页申请的,为了管理这些内存大块,把这些内存块的属性信息填好管理起来,切成对应小块内存,挂在Span的自由链表下,总体看起来就是一条双向链表,每个非头节点下面再挂着一条链表。如下图:
(2)、Span结构要有Span挂着内存的页号,和总页数,分配出去使用的计数,是否正在分配出去等,这些在CentralCache把内存还给PageCache的时候要使用。
// Span类存储这这块内存的各种信息
struct Span
{
PAGE_ID _pageId = 0; // 大块内存属于的页号
size_t _n = 0; // 页的数量
Span* _next = nullptr; // 用带头双向链表的结构组织
Span* _prev = nullptr;
void* _freeList = nullptr; // 按页取整向下申请一大块,切成小块给到上面
size_t _objSize = 0; // 挂着小块内存的大小
size_t _useCount = 0; // 切好的小块内存,被分配给ThreadCache的计数
bool _isUse = false; // 当前Span对象下的内存是否在提供给上层使用
};
组织方式:带头双向循环链表,需要带个桶锁;
// 带头双向链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
Span* Begin()
{
return _head->_next;
}
Span* End()
{
return _head;
}
// 头插
void PushFront(Span* newSpan)
{
Insert(Begin(), newSpan);
}
// 在pos处插入
void Insert(Span* pos, Span* newSpan)
{
assert(pos && newSpan);
Span* prev = pos->_prev;
newSpan->_next = pos;
newSpan->_prev = prev;
prev->_next = newSpan;
pos->_prev = newSpan;
}
// 弹出一个Span对象
Span* PopFront()
{
Span* front = _head->_next;
Erase(front);
return front;
}
// 删除一个Span
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* next = pos->_next;
Span* prev = pos->_prev;
next->_prev = prev;
prev->_next = next;
}
bool Empty()
{
return _head->_next == _head;
}
private:
Span* _head; // 头节点
public:
std::mutex _mtx; // 桶锁
};
(3)、返回对象内存给ThreadCache和归还内存给PageCache流程:
源码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include "CentralCache.h"
#include "PageCache.h"
// 全局单例对象类外初始化
CentralCache CentralCache::_sInst;
// 获取对应SpanList下一个非空的Span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 先检查当前链表下的自由链表有没有挂着空闲内存
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
return it;
it = it->_next;
}
// 没有空闲的了,得找PageCache要了
// 先把桶锁解了,这时候可能别的线程会归还对象
list._mtx.unlock();
// 把PageCache的大锁加上
PageCache::GetInstance()->_pageMutex.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
// Span对象挂着的小块内存的大小
span->_objSize = size;
span->_isUse = true;
PageCache::GetInstance()->_pageMutex.unlock();
// 起始地址,要进行+所以类型是char*
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t totalSize = span->_n << PAGE_SHIFT;
// 结束地址
char* end = start + totalSize;
// 切分成小块内存
span->_freeList = (void*)start;
void* tail = start;
start += size;
// size_t i = 1; // 测试切割个数
while (start < end)
{
// i++;
NextObj(tail) = start;
tail = start;
start += size;
}
// cout << i << endl;
// 处理最后一小块指向
NextObj(tail) = nullptr;
// 切分完成,要把对象插入SpanList中就要把锁加回来了
list._mtx.lock();
list.PushFront(span);
return span;
}
// 从CentralCache获取一定数量的对象给ThreadCache
// 返回值是实际返回的个数
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t bacthNum, size_t size)
{
// 先找到该去哪个链表找
size_t index = SizeClass::Index(size);
// 加上桶锁
_spanList[index]._mtx.lock();
Span* span = GetOneSpan(_spanList[index], size);
// 对象和自由链表不能为空
assert(span);
assert(span->_freeList);
// 遍历span->_freeList看看够不够bacthNum
size_t actualNum = 1;
start = span->_freeList;
end = start;
// 遍历自由链表,计算个数
while (actualNum < bacthNum && NextObj(end) != nullptr)
{
actualNum++;
end = NextObj(end);
}
// 剩下的继续挂在自由链表下
span->_freeList = NextObj(end);
// 处理返回区间的结尾
NextObj(end) = nullptr;
// 处理计数
span->_useCount += actualNum;
_spanList[index]._mtx.unlock();
return actualNum;
}
// 将一定数量的对象归还对应Span对象
void CentralCache::ReleaseListToSpan(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanList[index]._mtx.lock();
while (start != nullptr)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
// 头插
NextObj(start) = span->_freeList;
span->_freeList = start;
span->_useCount--;
if (span->_useCount == 0)
{
// 把对象弹出来,准备还给PageCache
_spanList[index].Erase(span);
// Span对象停止给上层使用
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
// 解桶锁
_spanList[index]._mtx.unlock();
PageCache::GetInstance()->_pageMutex.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMutex.unlock();
// 加上桶锁
_spanList[index]._mtx.lock();
}
start = next;
}
_spanList[index]._mtx.unlock();
}
(1)、PageCache的结构就是129个SpanList的双向带头循环链表构成的,只管理不大于128页的Span对象,按页的数量进行下标映射;需要一把大锁;和一个Span的定长对象池脱离malloc,还有一个页号和Span对象映射的结构(哈希表实现的,锁竞争消耗过大,分析和优化在下面讲解)。
类定义(_idSpanMap可以使用STL哈希表,优化使用基数树,下面分析):
#pragma once
#include "Common.h"
#include "ObjectPool.h"
#include "PageMap.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
// 提供获取对象到Span的映射
Span* MapObjectToSpan(void* obj);
// 获取一个k页的Span
Span* NewSpan(size_t k);
// 归还Span对象给PageCache
void ReleaseSpanToPageCache(Span* span);
private:
PageCache()
{}
PageCache(const PageCache&) = delete;
PageCache& operator = (const PageCache&) = delete;
private:
SpanList _spanList[NPAGES]; // 一次申请超过128页,直接先系统申请
//std::unordered_map _idSpanMap; // 页号和Span对象的映射
TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap; // 页号和Span对象的映射,底层基数树
ObjectPool _spanPool; // Span的对象池
static PageCache _sInst; // 全局单例对象
public:
std::mutex _pageMutex; // 涉及合并会影响其他,所以整个PageCache加一把大锁
};
(2)、分配Span对象给CentralCache和回收Span对象合并的流程:
#define _CRT_SECURE_NO_WARNINGS 1
#include "PageCache.h"
PageCache PageCache::_sInst;
// 提供获取对象到Span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
assert(obj);
// 换成基数树,底层结构不会改变
// 逻辑上对Span对象读写是分离的,不会同时读写
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
Span* span = (Span*)_idSpanMap.get(id);
assert(span != nullptr);
return span;
// 底层使用哈希表,不加锁,结构可能会改变
/*std::unique_lock lock(_pageMutex);
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
auto it = _idSpanMap.find(id);
if (it != _idSpanMap.end())
return it->second;
else
{
assert(false);
return nullptr;
}*/
}
// 获取一个k页的Span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
Span* kspan = _spanPool.New();
// 大于128页无法管理,直接还给堆,其他字段不用填
kspan->_pageId = (PAGE_ID)(ptr) >> PAGE_SHIFT;
kspan->_n = k;
// 建立映射
// _idSpanMap[span->_pageId] = span;
_idSpanMap.set(kspan->_pageId, kspan);
return kspan;
}
// 小于128,看看有没有现成Span对象
if (!_spanList[k].Empty())
{
Span* kSpan = _spanList[k].PopFront();
// 为k页都建立映射
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
// _idSpanMap[kSpan->_pageId + i] = kSpan;
_idSpanMap.set(kSpan->_pageId + i, kSpan);
}
return kSpan;
}
// 没有现成的,找大页分成小页
for (size_t i = k + 1; i <= NPAGES - 1; ++i)
{
if (!_spanList[i].Empty())
{
Span* nSpan = _spanList[i].PopFront();
Span* kSpan = _spanPool.New();
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId = nSpan->_pageId + k;
nSpan->_n -= k;
// 没有分配给CentralCache,挂在对应链表下,只需映射头和尾
_spanList[nSpan->_n].PushFront(nSpan);
// _idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap.set(nSpan->_pageId, nSpan);
// _idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
_idSpanMap.set(nSpan->_pageId + nSpan->_n - 1, nSpan);
// 分配给CentralCache会分成各种小块给到不同的ThreadCache,所以每一页都要映射
for (PAGE_ID j = 0; j < k; ++j)
{
// _idSpanMap[kSpan->_pageId + j] = kSpan;
_idSpanMap.set(kSpan->_pageId + j, kSpan);
}
return kSpan;
}
}
// 走到这得去先系统申请128页
void* ptr = SystemAlloc(NPAGES - 1);
Span* bigSpan = _spanPool.New();
bigSpan->_pageId = (PAGE_ID)(ptr) >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
_spanList[bigSpan->_n].PushFront(bigSpan);
// 递归再调用,代码复用
return NewSpan(k);
}
// 归还Span对象给PageCache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 大于128页直接还给系统
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)((span->_pageId) << PAGE_SHIFT);
SystemFree(ptr);
_spanPool.Free(span);
return;
}
// 往前合并
while (1)
{
PAGE_ID prevId = span->_pageId - 1;
/*auto ret = _idSpanMap.find(prevId);
if (ret == _idSpanMap.end())
break;
Span* prevSpan = ret->second;*/
Span* prevSpan = (Span*)_idSpanMap.get(prevId);
if (prevSpan == nullptr)
break;
// 该对象被使用,中止
if (prevSpan->_isUse == true)
break;
// 加起来太大不能管理,中止
if (prevSpan->_n + span->_n > NPAGES - 1)
break;
// 合并
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
// 弹出
_spanList[prevSpan->_n].Erase(prevSpan);
// 释放
_spanPool.Free(prevSpan);
}
// 往后找
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n;
/*auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end())
break;
Span* nextSpan = ret->second;*/
Span* nextSpan = (Span*)_idSpanMap.get(nextId);
if (nextSpan == nullptr)
break;
if (nextSpan->_isUse == true)
break;
if (nextSpan->_n + span->_n > NPAGES - 1)
break;
span->_n += nextSpan->_n;
_spanList[nextSpan->_n].Erase(nextSpan);
_spanPool.Free(nextSpan);
}
// 合并挂起来
_spanList[span->_n].PushFront(span);
span->_isUse = false;
// 建立映射
/*_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;*/
_idSpanMap.set(span->_pageId, span);
_idSpanMap.set(span->_pageId + span->_n - 1, span);
}
(1)、多线程,多轮次,多次数,测试代码:(页号和Span*的映射采用STL哈希表)
#define _CRT_SECURE_NO_WARNINGS 1
#include "ConcurrentAlloc.h"
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector vthreads(nworks);
std::atomic malloc_costtimes;
std::atomic free_costtimes;
for (size_t i = 0; i < nworks; ++i)
{
vthreads[i] = std::thread(
[&]() {
std::vector v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t k = 0; k < ntimes; ++k)
{
//v.push_back(malloc((16 + k) % 8192 + 1));
v.push_back(malloc(16));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t k = 0; k < ntimes; k++)
{
free(v[k]);
}
size_t end2 = clock();
v.clear();
malloc_costtimes += (end1 - begin1);
free_costtimes += (end2 - begin2);
}
}
);
}
for (auto& e : vthreads)
{
e.join();
}
printf("%u个线程并发执行%u轮,每轮malloc %u次: 花费: %u ms\n",
nworks, rounds, ntimes, (size_t)malloc_costtimes);
printf("%u个线程并发执行%u轮,每轮free %u次: 花费: %u ms\n",
nworks, rounds, ntimes, (size_t)free_costtimes);
printf("%u个线程并发malloc和free %u次: 总计花费: %u ms\n",
nworks, nworks * rounds * ntimes, (size_t)(malloc_costtimes + free_costtimes));
}
void BenchmarkConcurrentAlloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector vthreads(nworks);
std::atomic malloc_costtimes;
std::atomic free_costtimes;
for (size_t i = 0; i < nworks; ++i)
{
vthreads[i] = std::thread(
[&]() {
std::vector v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t k = 0; k < ntimes; ++k)
{
//v.push_back(ConcurrentAlloc((16 + k) % 8192 + 1));
v.push_back(ConcurrentAlloc(16));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t k = 0; k < ntimes; k++)
{
ConcurrentFree(v[k]);
}
size_t end2 = clock();
v.clear();
malloc_costtimes += (end1 - begin1);
free_costtimes += (end2 - begin2);
}
}
);
}
for (auto& e : vthreads)
{
e.join();
}
printf("%u个线程并发执行%u轮,每轮ConcurrentMalloc %u次: 花费: %u ms\n",
nworks, rounds, ntimes, (size_t)malloc_costtimes);
printf("%u个线程并发执行%u轮,每轮ConcurrentFree %u次: 花费: %u ms\n",
nworks, rounds, ntimes, (size_t)free_costtimes);
printf("%u个线程并发ConcurrentMalloc和ConcurrentFree %u次: 总计花费: %u ms\n",
nworks, nworks * rounds * ntimes, (size_t)(malloc_costtimes + free_costtimes));
}
int main()
{
size_t nworks = 5;
size_t ntimes = 10000;
size_t rounds = 10;
BenchmarkConcurrentAlloc(ntimes, nworks, rounds);
cout << endl;
cout << "**********************************************" << endl;
cout << endl;
BenchmarkMalloc(ntimes, nworks, rounds);
return 0;
}
(2)、测试结果:
只申请16字节:
随机申请各个字节:
(3)、结果分析:使用VS2019自带性能探查器:观察函数占用时间:
分析:使用STL哈希表时,底层结构可能因为结构负载因子过大,扩容导致底层结构改变,虽然一个双向链表下逻辑下不可能同时读写,读写是分离的,但是别的双向链表会同时写,可能导致结构改变,读取出错(红黑树会旋转),所以使用STL容器需要加锁。
(1)、方向:读取时映射,加锁消耗;那底层就使用结构不会改变的基数树作为映射的结构。
(2)、基数树原理:以一层和2两层(32位系统为例)
一层:
两层:
总结:一层和多层的区别就是:一层必须在构造的时候全部开辟出来,而多层可以在需要建立映射的时候才把对应的下层数组再动态开辟出来;32位系统可以直接开辟出来消耗大约2M左右,但是64位系统就必须3层以上进行管理映射;
基数树是页号和下标对应,底层数组不可能进行扩容,只有创建和没有创建,所以上层在进行读取时,线程不用加锁。
(3)、优化结果:
只申请16字节:
随机申请字节数:
结果:当前项目的性能在多线程环境下,优于malloc。
大家也可以了解malloc的底层实现是ptmalloc,简而言之:ptmalloc的申请和释放第一步都得加锁,后分配的内存先释放,不适合长期的管理内存,会导致内存暴增,对内存泄漏非常敏感。
最后祝大家学到自己所学到的,学的都会。