目录
一,项目介绍
1,项目简介
当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目TCMalloc(Thread-Caching Malloc),即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free)。
2,涉及知识
C/C++ 、数据结构(链表、哈希桶,基数数)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识
3,TCMlloc 介绍
TCMalloc 是 Google 对 C 的 malloc() 和 C++ 的 operator new 的自定义实现,用于在我们的 C 和 C++ 代码中进行内存分配。 TCMalloc 是一种快速、多线程的 malloc 实现。
TCMlloc 的基本结构
我们这个项目是把tcmalloc最核心的框架简化后拿出来,模拟实现出一个自己的高并发内存池,目的就是学习TCMlloc的精华;
二,设计定长内存池
1,什么是内存池
内存池是指程序预先从操作系统 申请一块足够大内存 ,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取 ;
同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池 。当程序退出( 或者特定时间 ) 时,内存池才将之前申请的内存真正释放;
1,池化技术
所谓 “ 池化技术 ” ,就是程序先向系统申请过量的资源,然后自己管理,以备不时之需。之所以要申请过量的资源,是因为每次向系统申请该资源都有较大的开销,不如提前申请好了,这样使用时就会变得非常快捷,大大提高程序运行效率;
池化技术常见的应用有:线程池、数据库连接池、内存池等。
2,内存池主要解决的问题
当然是解决效率的问题了
比如现在有385byte的空间,但是我们要申请超过256byte的空间却申请不出来
因为这两块空间碎片化,不连续了,这种碎片叫做外碎片
外碎片:是一些空闲的连续内存区域太小,这些内存空间不连续,以至于合计的内存足够,但是不能满足一些的内存分配申请需求。
2,定长内存池介绍
在定长内存池中,所有块的大小都是相同的,所以分配内存时只需从池中选择一个空闲块;
释放内存时只需要将其标记为可用。而这也避免了因频繁分配和释放不同大小的内存块而产生的内存碎片问题。
3,定长内存池的实现
固定大小的内存申请释放需求
特点:
1,性能达到极致
2,不考虑内存碎片问题
1,先开辟一大段的空间并且用指针_memeory指向
2,又为了解决_memeory指向空间的小 < 申请空间的大小,引入了_remainBytes记录剩余空间
3,申请空间,就会从这里面拿,而释放的空间,我选择用_freeList指针指向这段空间,并一一链接,做到回收并重复利用
4,而在_freeList连接的空间里,我用前4或8个字节,记录下一个空间的地址(这就避免了重新定义变量去记录)
5,申请空间需要先从_freeList里面拿,看看是否有回收的空间可以重复利用
4,objectPool.h
//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);//绕过malloc,直接从内核申请内存
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
template<class T>
class objectPool
{
public:
T* New()
{
T* obj = nullptr;
//优先使用归还回来的内存块对象,重复利用
if (_freeList)//不为空
{
void* next = NextObj(_freeList);
obj = (T*)_freeList;//_freeList里面保存的是第一个内存块的地址
_freeList = next;//从自由链表头删一个对象
}
else
{
// 剩余内存不够一个对象大小时,则重新开大块空间
if (_remainBytes < sizeof(T))
{
_remainBytes = 8 * 1024;
//这里相当于是申请一页物理内存
_memory = (char*)SystemAlloc(_remainBytes >> 13);//右移13位相当于除以8KB,得到的数值单位就是页
if (_memory == NULL)
{
throw std::bad_alloc();// 直接抛出异常
}
}
obj = (T*)_memory;
//如果对象使用的类型的大小 比指针小,就给他一个指针大小的字节数(多开空间),便于后续回收
size_t objsize = (sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T));
_remainBytes -= objsize;
_memory += objsize;
}
// 定位new,显式调用T的构造函数初始化
new(obj)T;
return obj;
}
//释放对象
void Delete(T* obj)
{
obj->~T();//显式调用T的析构函数清理对象
//头插
NextObj(obj) = _freeList;//obj的前 指针大小 的字节数里保存下一个内存的地址
_freeList = obj;
}
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
private:
char* _memory = nullptr;//指向大块内存的指针
size_t _remainBytes = 0;//大块内存在切分过程中剩余字节数
void* _freeList = nullptr;//用来保存和指向归还内存的头指针,连成一个链表
};
windows下向堆申请页为单位的大块内存的接口->VirtualAlloc
Linux下向堆申请页为单位的大块内存的接口->brk和mmap
为了避免使用 malloc ,程序里面申请空间直接调用接口向堆申请;
三,高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。
malloc本身其实已经现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题,Malloc本身其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程高并发的场景下更胜一筹,所以这次我们实现的内存池需要考虑以下几方面的问题;
1,性能问题
2,多线程环境下,锁竞争问题
3,内存碎片问题
图示:
concurrent memory pool 主要由以下 3 个部分构成:
1,thread cache:线程缓存是每个线程独有的,用于小于256KB的内存的分配,线程从这里申请内存不需要加锁,每个线程独享一个cache,这也就是这个并发线程池高效的地方。
2,central cache:中心缓存是所有线程所共享,thread cache是按需从central cache中获取的对象。central cache合适的时机回收thread cache中的对象,避免一个线程占用了太多的内存,而其他线程的内存吃紧,达到内存分配在多个线程中更均衡的按需调度的目的。
central cache是存在竞争的,所以从这里取内存对象是需要加锁,首先这里用的是桶锁,其次只有thread cache的没有内存对象时才会找central cache,所以这里竞争不会很激烈。
3,page cache:页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,central cache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给central cache。当一个span的几个跨度页的对象都回收以后,page cache 会回收central cache满足条件的span对象,并且合并相邻的页,组成更大的页,缓解内存碎片 的问题。
四,thread cache
thread cache 是哈希桶结构,每个桶是一个按桶位置 映射 大小的内存块对象的 自由链表 。每个线程都会有一个thread cache 对象,这样每个线程在这里获取对象和释放对象时是 无锁的
图示结构:
1,实现思路
1,由于后面需要重复包含某些头文件,这里可以把它们放在Comment.h中,方便后面调用
同理,也需要把 freelist 自由链表封装成类;
2,而在ThreadCache中需要Allocate和Deallocate接口,因为是哈希桶结构(挂的是自由链表)
所以申请空间,会先在自由链表中拿,不够或没有会去找centrallcache拿,则又需要一个FetchFromcentrallcache接口,释放空间的话会先返回自由链表中挂起,多了又会返回给centrallcache;
3,同时需要考虑一个比较重要的就是哈希桶的映射规则,
计算对齐数(无法避免内碎片),和计算几号桶
static const size_t MAX_SIZE = 256 * 1024;// 最大字节数
static const size_t NFREELISTS = 208;// 最大自由链表数
4,为了保证每一个线程都有一个ThreadCache,且不会相互影响,这时需要引入TLS thread local storage static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
(线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这就保持了数据的线程独立性,而熟知的全局变量,是所以线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度)
5,为了后面测试代码,这里需要再整体封装一层,在ConcurrentAlloc.h中实现ConcurrentAlloc和ConcurrentFree
2,Comment.h
#pragma once
#include <iostream>
#include <vector>
#include <windows.h>
#include <new>
#include <assert.h>
#include <thread>
static const size_t MAX_SIZE = 256 * 1024;// 最大字节数
static const size_t NFREELISTS = 208;// 最大自由链表数
using std::cout;
using std::endl;
// 这里是引用返回,这里本质就是返回obj前面4个字节的地址
static void*& NextObj(void* obj) {
return *(void**)obj;
}
// 管理内存块的自由链表
class FreeList {
public:
// 头插
void push(void* obj) {
assert(obj);
NextObj(obj) = _freeList;
_freeList = obj;
}
// 头删
void* pop() {
assert(_freeList);
void* obj = _freeList;
_freeList = NextObj(obj);
return obj;
}
bool empty() {
return _freeList == nullptr;
}
private:
void* _freeList = nullptr;// 管理一个一个的小对象
};
// 映射对齐规则
class SizeClass {
public:
// 计算对齐数
static inline size_t _RoundUp(size_t size, size_t Align_Num) {
return ((size + Align_Num - 1) & ~(Align_Num - 1));// 位运算的效率更高
}
// 把函数定义成静态成员函数,就可以直接调用
static size_t RoundUp(size_t size) {
if (size <= 128) {
return _RoundUp(size, 8);// 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 {
assert(false);// 直接报错
return -1;// 暂时不考虑254kb
}
}
// 计算几号桶
static size_t _IndexNum(size_t size,size_t Align_left) {
return ((size + (1 << Align_left) - 1) >> Align_left) - 1;
}
static size_t IndexNum(size_t size) {
int group_num[4] = { 16,72,128,184 };
if (size <= 128) {
return _IndexNum(size, 3);//根据对齐数->这里我直接传的是2几次方
}
else if (size <= 1024) {
return _IndexNum(size-128, 4)+group_num[0];
}
else if (size <= 8 * 1024) {
return _IndexNum(size-1024,7)+group_num[1];
}
else if (size <= 64 * 1024) {
return _IndexNum(size-8*1024, 10) + group_num[2];
}
else if (size <= 256 * 1024) {
return _IndexNum(size-64*1024, 13) + group_num[3];
}
else {
assert(false);// 直接报错
return -1;// 暂时不考虑254kb
}
}
};
3,ConcurrentAlloc.h
#pragma once
#include "Common.h"
#include "ThreadCache.h"
static void* ConcurrentAlloc(size_t size)
{
// 通过TLS 每个线程无锁的获取自己的专属的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)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
4,ThreadCache.h
#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);
private:
FreeList _freeLists[NFREELIST];
};
//TLS thread local storage->使这个变量在本线程内中是全局变量,且不能被其他线程访问到
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
5,ThreadCache.cpp
#include "ThreadCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
// ...
return nullptr;
}
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size);//对齐数
size_t index = SizeClass::Index(size);//对应的桶
if (!_freeLists[index].Empty())
{
return _freeLists[index].Pop();
}
else
{
return FetchFromCentralCache(index, alignSize);
}
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 找对映射的自由链表桶,对象插入进入
size_t index = SizeClass::Index(size);
_freeLists[index].Push(ptr);
}
6,测试 UintTest.cpp
#include "Concurrent.h"
void Thread1() {
for (int i = 0; i < 5; i++) {
void* obj = ConcurrentAlloc(9);
//ConcurrentFree(obj, 9);
}
}
void Thread2() {
for (int i = 0; i < 5; i++) {
void* obj = ConcurrentAlloc(18);
//ConcurrentFree(obj, 18);
}
}
void TestThreadCache() {
std::thread t1(Thread1);
t1.join();
std::thread t2(Thread2);
t2.join();
}
int main()
{
TestThreadCache();
return 0;
}
可以看到这两条线程也是走通了;
7,映射规则
用位运算计算映射对齐数效率更高!
计算几号哈希桶也使用位运算!
五,CentrallCache
1,实现思路
central cache 也是一个 哈希桶 结构,他的哈希桶的映射关系跟 thread cache 是一样的。
不同的是他的每个哈希桶位置挂是SpanList 链表结构,每个映射桶下面的 span 中的大内存块被按映射关系切成了一个个小内存块对象挂在span 的自由链表中
1,首先需要把span和spanList实现出来, 其中spanList是一个带头双向循环链表;
2,由于存在多线程共同竞争的问题,这里可以使用单例模式中的饿汉模式
3,由于上一层,thread cache申请空间不够,会在centrall cache中拿空间,所以需要完善并实现FetchFromcentrallcache
4,thread cache拿空间的时候,需要拿一批空间,这里具体需要拿多少个,可以使用慢调节算法
在Sizeclass(映射规则)中实现一个NumMoveSize,size_t n = MAX_SIZE / size;
if(n<2)就返回2,if(n>512)就返回512,其他就返回n
可以再控制一下慢调节算法,在自由链表中加入max_num变量,使其第一次拿给一个,第二次拿给二个(也可以增长的再快点)
size_t batchNum = min(_freeLists[Index].MaxNum(), SizeClass::NumMoveSize(size));
也不是想拿几个就拿几个,不够的话就是有多少给多少,size_t actualNum;
5,thread cache拿空间的时候,从代码中来看是一段自由链表,所以又需要void* start = nullptr, * end = nullptr;记录头尾,返回是第一个小内存块,而多的就需要挂起,
6,centrall cache中需要实现一个函数,从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
因为存在多个线程同时竞争的问题,所以需要加锁
给对象申请一批空间,应该从非空的span中拿,又需要实现一个找非空spand函数,
注意:如果是一开始的情况,是找不到非空span的则又需要从page cache中拿空间
Span* GetOneSpan(SpanList& list, size_t batchNum);// 暂时不实现
7,FetchRangeObj它的返回值,应该是size_t,返回的是thread cache中centrall cache中拿的数量(并不是想拿多少就拿多少,不够的话就是有多少给多少)
2,CentrallCache.h
#pragma once
#include "Common.h"
// 单例模式
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t byte_size);
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
private:
SpanList _spanLists[NFREELIST];
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;//单例模式——>饿汉模式
};
3,CentrallCache.cpp
#include "CentralCache.h"
CentralCache CentralCache::_sInst;
// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// ...
return nullptr;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t CentrallCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::IndexNum(size);
//加桶锁,保证线程安全
_spanList[index]._mtx.lock();
//获取一个非空的span
Span* span = GetOneSpan(_spanList[index], size);
assert(span);
assert(span->_freeList);
// 从span中获取batchNum个对象
// 如果不够batchNum个,有多少拿多少
start = span->_freeList;
end = start;
size_t i = 0;
size_t actualNum = 1;
while (NextObj(end) && i < batchNum-1)
{
end = NextObj(end);
actualNum++;
}
//记录给出的内存对象数量,方便后续释放
span->_useCount += actualNum;
span->_freeList = NextObj(end);
NextObj(end) = nullptr;
_spanList[index]._mtx.unlock();
return actualNum;
}
4,FetchFromCentralCache
ThreadCache从中心缓存获取对象
// 从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//慢反馈调节算法
// 1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完
// 2、如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
// 3、size越大,一次向central cache要的batchNum就越小
// 4、size越小,一次向central cache要的batchNum就越大
size_t batchNum = min(_freeList[index].MaxSize(), SizeClass::NumMoveSize(size));
if (batchNum == _freeList[index].MaxSize())
{
_freeList->MaxSize() += 1;
}
void* start = nullptr;
void* end = nullptr;
//实际获得的个数
size_t actual = CentrallCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actual > 0);
if (actual == 1)
{
return start;
}
else
{
_freeList[index].PushRange(NextObj(start), end,actual);
return start;
}
return nullptr;
}
5,NumMoveSize
一次thread cache从中心缓存获取多少个对象
// 一次thread cache从中心缓存获取多少个小对象
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_SIZE / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
6,Span结点的结构
// 管理多个连续页大块内存跨度结构
struct Span
{
PAGE_ID _pageId = 0; // 大块内存起始页的页号
size_t _n = 0; // 页的数量
Span* _next = nullptr; // 双向链表的结构
Span* _prev = nullptr;
size_t _useCount = 0; // 切好小块内存,被分配给thread cache的计数
void* _freeList = nullptr; // 切好的小块内存的自由链表
};
7,SpanList
//双向链表
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* span)
{
Insert(Begin(), 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;
}
//擦除
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* next = pos->next;
Span* prev = pos->prev;
prev->next = next;
next->prev = prev;
}
//头删
Span* popFront()
{
Span* span = _head->next;
Erase(span);
return span;
}
//判空
bool Empty()
{
return _head->next == _head;
}
private:
Span* _head = nullptr;
public:
std::mutex _mtx;//桶锁
};
thread cache在centrall cache中拿数据,其实是在centrall cache中找到一个非空span,拿一段空间给thread cache,从代码上看就是一段自由链表,再范围头插即可;
最后,Span 的规模取决于 SizeClass,都以页为单位,为了后续判断 Span 是否在物理上是相邻的,在 PageHeap 分配 Span 时就给它一个页号PAGE_ID,作为 Span 的唯一标记,它的值等于物理地址除以 2^13。
在 32 位和 64 位下的进程地址空间大小是不同的,后者的地址无法直接用一个unsigned int存储 [0,2^51],需要用 64 位保存。使用条件编译以支持在 32/64 位下使用合适的变量存储页号。
// Common.h
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef unsigned int PAGE_ID;
#else
// Linux
#endif
主要是要感受代码,更多理解在思考中升华!
六,PageCache
1,实现思路
PageHeap 的结构和 CentralCache 类似,同样用双链表组织 Span。不同的是 PageHeap 哈希桶的下标按 Span 的页号映射。第 x 号桶挂的都是 x 页 Span。在 TCmalloc 中,对于不大于 256KB 内存申请的情况,页号到 Span 的映射有 128 个,128 个 Page 可以被切成 128*8KB/256KB=4 个 256KB 的对象,这个经验值可以满足大多数情况。为了方便映射,弃用下标为 0 的位置。
1,因为还是存在多进程竞争的问题,所以这里还是把page cache设计成单列模式
2,虽然page cache也是哈希桶结构,但它是直接映射的,1page -> 1page,128page->page
3,在上一层CentrallCache 中没有span的时候,会从page cache通过NewSpan拿一个span,但具体拿几页的span,还是需要通过慢增长算法-> NumMovePage
4,CentrallCache在得到这个大块内存span之后,需要把它切分成自由链表的形式,并挂在CentrallCache对应的哈希桶中
char* start = (char*)(span->_pageId << PAGE_SHIFT);// 起始地址 = 地址 * 8 * 1024
size_t bytes = span->_n << PAGE_SHIFT;// 页号* 8 * 1024
char* end = start + bytes;
5,通过上面的思路,page cache中就需要实现NewSpan函数(获取某一页的大块内存)
如果在page cache的哈希桶对应的k页上有大块内存,则直接PopFront
否则就会从k+1页开始遍历,找一块大块内存,并切分成k页的span + n-k页的span
走到这里就说明 page cache为空,就需要从系统中申请内存(windows下,SystemAlloc)
2,PageCache.h
#pragma once
#include "Common.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
// 申请一个span
Span* NewSpan(size_t k);
std::mutex _pageMtx;
private:
SpanList _spanLists[NPAGES];
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
1,毫无疑问page cache中肯定是需要加锁的
2,但是不能设计成桶锁,因为page cache中会涉及到页的合并,要2page的没有它就会去找3page的,会将这个3page的切分1page + 2page的2个大块内存
同时也可能会存在2个线程,1个线程要1page,另一个线程要2page,需要同时拆分大page的情况
4,因为NewSpan中肯定会涉及多线程竞争的问题,则这里可以直接考虑把锁加在外面
central cache是通过GetOneSpan得到一个k页的span,则这里就可以先把centrall cache的桶锁解掉,这样如果有其他线程释放内存对象回来,就不会阻塞
3,PageCache.cpp
#include "PageCache.h"
PageCache PageCache::_sInst;
// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
// 先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
return _spanLists->PopFront();
}
// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
for (size_t i = k+1; i < NPAGES; ++i)
{
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
// 在nSpan的头部切一个k页下来
// k页span返回
// nSpan再挂到对应映射的位置
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
_spanLists[nSpan->_n].PushFront(nSpan);
return kSpan;
}
}
// 走到这个位置就说明后面没有大页的span了
// 这时就去找堆要一个128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
1,当CentralCache向PageCache申请内存时,page Cache先检查的是对应位置有没有span,若没有则向更大的页寻找一个span,例如申请的是4页,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page的span分裂成一个4页的page span 和一个6页page span。
2,若找到_spanList[128]都没有合适的span,则向系统使用VirtualAlloc或者mmap,brk等方式申请128页page span关在自由链表中,在重复第一点中的步骤,对其进行切分。
3,Central Cache 和 Page Cache的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,Central Cache中的哈希桶,是按跟Thread Cache一样大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
4,测试对齐
测试代码
void AlignTest() // 测试对齐
{
void* ptr1 = ConcurrentAlloc(6);
void* ptr2 = ConcurrentAlloc(8);
void* ptr3 = ConcurrentAlloc(10);
std::cout << ptr1 << std::endl;
std::cout << ptr2 << std::endl;
std::cout << ptr3 << std::endl;
}
页号等于地址/8k;
七,ThreadCache 释放
1,ConcurrentFree
static void ConcurrentFree(void* ptr, size_t size)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
2,ThreadCache.h
#pragma once
#include"Comment.h"
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t alignSize);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeList[NFREELISTS];//自由链表数组
};
//TLS thread local storage->使这个变量在本线程内中是全局变量,且不能被其他线程访问到
static __declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
3,ThreadCache.cpp
//释放内存
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_SIZE);
//找到映射的自由链表的桶,插入进去
size_t index = SizeClass::IndexNum(size);
_freeList[index].push(ptr);
// 当链表长度大于一次批量申请的内存时就开始还一段list给CentrallCache
if (_freeList[index].Size() >= _freeList[index].MaxSize())
{
ListTooLong(_freeList[index], size);
}
}
// 释放对象时,链表过长时,回收内存回到中心缓存
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//list里批量删除
list.PopRange(start, end, list.MaxSize());
///将一定数量的对象释放到CentrallCache里的span
CentrallCache::GetInstance()->ReleaseListToSpans(start, size);
}
4,释放内存流程
还回来的内存挂接在Thread Cache中对应的Free List中,当其中一个Free List中的Size(当前的内存块数)大于等于Max Size时,从该Free List取出Max Size个内存归还到Central Cache,但是每一块小内存都可能来自于不同的Span,这里就要调用MapObjectToSpan()接口根据每块小内存的起始地址计算出它所对应的页号,通过一个基数树映射根据页号找到所对应的Span,那么就可以确保每一小块内存都能归还到之前所切出来的对应的Span中,当Central Cache中的每一大块Span里面有一个usecount,他记录的是分配出去的内存块数,当他为0时,说明该span对象之前借出去的小内存都换回来了,这时就可以将该span归还给Page Cache,通过PageId(页号)和n(页数),进行在Page Cache中向前和向后合并,形成更大的页,当来缓解内存碎片问题。
5,介绍函数
ThreadCache::Deallocate
//释放内存
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_SIZE);
//找到映射的自由链表的桶,插入进去
size_t index = SizeClass::IndexNum(size);
_freeList[index].push(ptr);
// 当链表长度大于一次批量申请的内存时就开始还一段list给CentrallCache
if (_freeList[index].Size() >= _freeList[index].MaxSize())
{
ListTooLong(_freeList[index], size);
}
}
ptr 是内存指针,size 为内存大小,先要找到所映射的自由链表然后直接插入即可
如果此桶下的自由链表的长度大于等于一次批量申请的内存就要将此自由链表的部分内存释放给中心缓存;
ThreadCache::ListTooLong
// 释放对象时,链表过长时,回收内存回到中心缓存
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//list里批量删除
list.PopRange(start, end, list.MaxSize());
///将一定数量的对象释放到CentrallCache里的span
CentrallCache::GetInstance()->ReleaseListToSpans(start, size);
}
list 是ThreadCache下要释放内存的桶,size 是内存大小,先批量删除list里的内存,然后再释放给中心缓存
八,CentrallCache 释放
1,CentrallCache.h
#pragma once
#include"Comment.h"
//单例模式->饿汉模式->线程安全
class CentrallCache
{
public:
static CentrallCache* GetInstance()
{
return &_sInst;
}
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t byte_size);
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
// 将一定数量的对象释放到span
void ReleaseListToSpans(void* start, size_t size);
private:
SpanList _spanList[NFREELISTS];//与ThreadCache映射规则一致
private:
CentrallCache() {};//构造函数私有化
CentrallCache(const CentrallCache&)=delete;//禁掉拷贝构造函数
static CentrallCache _sInst;
};
2,CentrallCache.cpp
// 将一定数量的对象释放到span
void CentrallCache::ReleaseListToSpans(void* start, size_t size)
{
//先找到那个桶
size_t index = SizeClass::IndexNum(size);
_spanList[index]._mtx.lock();
while (start)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
//头插span里的自由链表
NextObj(start) = span->_freeList;
span->_freeList = start;
start = next;
span->_useCount--;
// 说明span的切分出去的所有小块内存都回来了
// 这个span就可以再回收给PageCache,PageCache可以再尝试去做前后页的合并
if (span->_useCount == 0)
{
_spanList[index].Erase(span);
span->_freeList = nullptr;
span->next = nullptr;
span->prev = nullptr;
// 释放span给PageCache时,使用PageCache的锁就可以了
// 这时把桶锁解掉
_spanList[index]._mtx.unlock();
// 释放空闲span回到Pagecache,并合并相邻的span
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanList[index]._mtx.lock();
}
}
_spanList[index]._mtx.unlock();
}
start 是接收指向释放内存的指针,size 是内存大小,老规矩还是先找到在哪一个哈希桶里,这里的结构是 spanList 链表结构,每个内存都是由 span 中分割出来的,所以我们用map映射来找到内存所对应的span方便后续插入,每插入一个小对象,span里的_useCount就会--,当_useCount为0时就说明 span 切分出去的所有小块内存都回来了,这时这个span就可以再回收给 pagecache;
九,PageCache 释放
1,PageCache.h
#pragma once
#include"Comment.h"
#include"objectPool.h"
#include"PageMap.h"
//单例模式->饿汉模式->线程安全
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
// 获取一个k页的span给CentrallCachen
Span* NewSpan(size_t k);
private:
SpanList _spanList[NPAGES];//span链表数组结构
private:
static PageCache _sInst;
objectPool<Span> _spanPool;
PageCache() {};//构造函数私有化
PageCache(const PageCache&)=delete;//禁掉拷贝构造函数
//std::unordered_map< PAGE_ID, Span*> _idSpanMap;
TCMalloc_PageMap1<32 - PAGE_SHIFT> _idSpanMap;
public:
std::mutex _pageMtx;//page锁
};
2,PageCache.cpp
// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于128页的内存直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId >> PAGE_SHIFT);
SystemFree(ptr);
//delete(ptr);
_spanPool.Delete(span);
return;
}
// 对span前后的页,尝试进行合并,缓解内存碎片问题
while (1)
{
PAGE_ID prevId = span->_pageId - 1;
//auto ret = _idSpanMap.find(prevId);
前面的页号没有,不合并了
//if (ret == _idSpanMap.end())
//{
// break;
//}
auto ret = (Span*)_idSpanMap.get(prevId);
if (ret == nullptr)
{
break;
}
// 前面相邻页的span在使用,不合并了
Span* prevSpan = ret;
if (prevSpan->_isUse == true)
{
break;
}
// 合并出超过128页的span没办法管理,不合并了
if (span->_n + prevSpan->_n > NPAGES - 1)
{
break;
}
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
//从链表中移除释放
_spanList[prevSpan->_n].Erase(prevSpan);
//delete(prevSpan);
_spanPool.Delete(prevSpan);
}
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n - 1;
//auto ret = _idSpanMap.find(nextId);
前面的页号没有,不合并了
//if (ret == _idSpanMap.end())
//{
// break;
//}
auto ret = (Span*)_idSpanMap.get(nextId);
if (ret == nullptr)
{
break;
}
Span* nextSpan = ret;
if (nextSpan->_isUse = true)
{
break;
}
if (span->_n + nextSpan->_n > NPAGES - 1)
{
break;
}
span->_n += nextSpan->_n;
_spanList[nextSpan->_n].Erase(nextSpan);
//delete(nextSpan);
_spanPool.Delete(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);
}
如果内存大小超过了256k则直接还给系统,否则我们需要将span的前后页进行合并,这是很重要的一步,可以解决内存碎片问题;
向前合并,就是将后面的 Span 加到前面,然后更新前面的页数。需要注意的是要保证地址是连续的,就是要判断后面的页号是否等于前面的页号+页数。例如图中前面的页号+页数是 4,刚好等于后面的页号 4,说明它们在被 PageHeap 切割时是连续的。向后合并也是一样的。可以在一个循环中不断合并,只要不符合相邻的条件就可以停止合并。
现在问题来了,span->_usedCount == 0的另一种情况是调用 PageHeap::NewSpan() 时新分配 Span 时,此时 Span 也是一个 Span 都没有被分配出去。为了让合并和切分的操作不冲突,用一个 bool 类型的变量_isUsed来标记 Span 是否已经被 CentralCache 使用,作为 Span 的成员。
3,测试
对应地,下面用单线程测试释放内存的流程。首先实现 ConcurrentFree() 最基本的功能,这个函数稍后要完善,参数 bytes 可以在函数内求得,这里只是为了测试运行起来。
// ConcurrentAlloc.h
static void ConcurrentFree(void* ptr, size_t bytes)
{
assert(ptr);
if (bytes > TC_MAX_BYTES) // 大于 256KB 的内存释放
{
// 归还给 PageHeap
}
//else
{
return TLSThreadCache_ptr->Deallocate(ptr, bytes);
}
}
void ConcurrentFreeTest()
{
void* ptr1 = ConcurrentAlloc(6);
void* ptr2 = ConcurrentAlloc(8);
void* ptr3 = ConcurrentAlloc(10);
void* ptr4 = ConcurrentAlloc(17);
void* ptr5 = ConcurrentAlloc(20);
ConcurrentFree(ptr1, 6);
ConcurrentFree(ptr2, 8);
ConcurrentFree(ptr3, 10);
ConcurrentFree(ptr4, 17);
ConcurrentFree(ptr5, 20);
}
可以看到 rightSpan 确实被合并到了 Span 上,页数也是对上了的。
十,大块内存的申请和释放
在这个项目中我们只处理了小于 256KB 的内存申请逻辑,下面对其进行补充。
我们规定 PageHeap 的最大规格 Span 是 128 页,即 128*8B=1024KB=1MB。ThreadCache 的最大规格 Object 是 256KB(256KB/8B=32 页)。那么小于 256KB(32 页)的内存请求由 ThreadCache 负责;大于 256KB(32 页)且小于 1MB(128 页)的内存请求由 PageHeap 负责;大于 1MB(32 页)的内存请求交给操作系统。
1,ConcurrentAlloc
//并发分配申请内存
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_SIZE)
{
//申请内存总量
size_t alignSize = SizeClass::RoundUp(size);
size_t kpage = alignSize >> PAGE_SHIFT;
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);
span->_objSize = size;
PageCache::GetInstance()->_pageMtx.unlock();
//通过页号转地址
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
return ptr;
}
else
{
//通过 TLS 每个线程无锁的获取自己的专属 ThreadCache 对象
if (pTLSThreadCache == nullptr)
{
static objectPool<ThreadCache> tcPool;
//pTLSThreadCache = new ThreadCache;
pTLSThreadCache = tcPool.New();
}
return pTLSThreadCache->Allocate(size);
}
}
如果申请的内存超过了256k,直接就是通过newspan向堆上申请空间;
2,ConcurrentFree
//并发释放内存
static void ConcurrentFree(void* ptr)
{
//通过地址找出所映射的span
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
if (size > MAX_SIZE)
{
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
如果释放的内存大于256k的话,直接通过ReleaseSpanToPageCache释放给堆;
十一,优化new,malloc
在这个项目里使用new最多的就是在page cache中,而为了使这个 项目脱离new,就可以使用上次的定长内存池,这所有的new 和delete都需要改造
ObjectPool<Span> _spanPool;// 定长内存池
十二,优化释放内存只传地址
只要在span中定义一个_objSize用来标识,记录每次切分的span大小就可以
至于为什么以前的释放要传size,主要是因为要区分是大于256kb,还是小于256kb的
之前是要传内存大小的,现在只穿地址即可,我们可以通过span找到大小;
十三,锁的优化 基数树
我们可以通过测试用例发现,运行过程中锁的时间占比是很大的;
这是之前的map加锁,后面我们用基数树来优化;
1,TCMalloc_PageMap1
一层基数树
/单层基数树
template <int BITS>
class TCMalloc_PageMap1 {
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);//对申请到的内存进行清理
}
void* get(Number k) const
{
if ((k >> BITS) > 0) //k 的范围不在 [0, 2^BITS-1]
{
return NULL;
}
return array_[k];//返回该页号对应的 span
}
void set(Number k, void* v)
{
assert((k >> BITS) == 0); //k 的范围必须在 [0, 2^BITS-1]
array_[k] = v; // 建立映射
}
private:
static const int LENGTH = 1 << BITS;//页的数目
void** array_;//存储映射关系的数组
};
数组的内容是 Span 的地址,下标对应着页号。非模板参数BITS对应着该平台下最大页号占的位数。LENGTH成员表示页数,其值是2的bits次方;
在32位平台中,BITS的值是 32-PAGE_SHIFT。1个Page 是8kb,LENGTH的值是 2^(32-13)=2^19,所以BITS的值是 19 ,表示存储页号最多要用 19 位。求出他的目的是先将内存申请好,以应付所有的情况。这个数组的大小是2^19*4b=2^20kb*2=2MB,是合理的。但是 64 位下这个数组大小是 2^(64-13)*8B=2^54B=2^24GB,需要三层基数划分。
二层基数树
//二层基数树
template <int BITS>
class TCMalloc_PageMap2
{
private:
static const int ROOT_BITS = 5; //第一层对应页号的前 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]; //第一层数组
public:
typedef uintptr_t Number;
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];//返回该页号对应 span 的指针
}
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; //建立该页号与对应 span 的映射
}
//确保映射 [start,start_n-1] 页号的空间是开辟好了的
bool Ensure(Number start, size_t n)
{
for (Number key = start; key <= start + n - 1;)
{
const Number i1 = key >> LEAF_BITS;
if (i1 >= ROOT_LENGTH) //页号超出范围
return false;
if (root_[i1] == NULL) //第一层 i1 下标指向的空间未开辟
{
//开辟对应空间
static objectPool<Leaf> leafPool;
Leaf* leaf = (Leaf*)leafPool.New();
memset(leaf, 0, sizeof(*leaf));
root_[i1] = leaf;
}
key = ((key >> LEAF_BITS) + 1) << LEAF_BITS; //继续后续检查
}
return true;
}
void PreallocateMoreMemory()
{
Ensure(0, 1 << BITS); //开辟第二层空间
}
};
在 32 位平台中,需要 19 位保存页号。将前 5 位和后 14 位分别作为第一层和第二层的键(Kye)
总共大小也是2MB的空间,二层基数树初始状态只需要为第一层数组开辟空间,第二层数组按需开辟。
基数树代替哈希表
由于测试的平台选择了 32 位,可以随便选几层基数树,这里将二层哈希表的实现放在
PageMap.h
中。Common.h 包含它以后,将 PageHeap 的哈希表换成基数树:
有了基数树,PageHeap::MapObjectToSpan() 就不用加锁了。
十四,效率测试
测试一
测试 4 个线程,10 轮,每轮 10000 次申请和释放固定大小的内存空间(同一个桶):
#define _CRT_SECURE_NO_WARNINGS 1
#include"ConcurrentAlloc.h"
//ntimes 一轮申请和释放内存的次数
//rounds 轮次
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)
{
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.load());
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks*rounds*ntimes, malloc_costtime.load() + free_costtime);
}
//单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(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)
{
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.load());
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks*rounds*ntimes, malloc_costtime.load() + free_costtime.load());
}
int main()
{
size_t n = 10000;
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n, 4, 10);
cout << endl << endl;
BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << endl;
return 0;
}
测试二
测试 4 个线程,10 轮,每轮 10000 次申请和释放不同大小的内存空间(放开第二条注释的代码):
十五,项目总结
此项目是一个高效的内存管理器 TCMalloc 简易实现,旨在提高内存分配和回收的性能。它主要采用了以下方法实现高并发内存分配器:
1,多层内存分配系统:项目实现了 ThreadCache、CentralCache 和 PageHeap 三个层次的内存分配,用于不同大小和频率的内存请求。这种设计可以减少与系统内存的直接交互,提高内存分配和释放的效率。
2,线程局部存储(Thread Local Storage,TLS):ThreadCache 作为线程私有缓存,减少了跨线程的内存分配冲突和锁的需求,从而提高了多线程环境下的性能。
3,内存碎片管理:通过 Span(连续的内存页组)和自由链表的管理,有效地处理了内存碎片问题,提高内存使用效率。
4,锁的策略和线程安全:在 PageHeap 和 CentralCache 中使用锁来保护共享资源,确保线程安全。这是在多线程环境中维护数据一致性和避免竞态条件的关键。
5,基数树映射:使用二层基数树(TCMalloc_PageMap2)来快速映射页号和 Span 地址,加速了内存地址到管理单元的映射过程。
6,大小类管理:SizeClass 类用于管理不同大小的内存请求,提供内存对齐和哈希桶索引功能,这有助于优化内存分配的速度和减少浪费。ThreadCache 和 CentralCache 都使用同一阶梯的 SizeClass,使得 ThreadCache 可以向 CentralCache 直接申请内存。
7,内存分配单元的动态调整:动态调整内存分配单位,以适应不同大小的内存请求,从而提高内存利用率。
8,性能测试:通过基准测试(Benchmark.cpp),这可以评估和分析 TCMalloc 在不同条件下的性能。
9,内存池:ObjectPool 用于减少频繁内存分配的开销,提高内存分配的效率。
个人收获
学习了内存管理器的思想,将内存从小到大分层,将一定数量的小内存让线程私有,按需申请和释放,这个过程是无锁的,是内存分配器在多线程环境下高效的原因之一。让较大的内存交给中央缓存和页堆管理,当它们的内存都超过一个阈值时,将内存归还给下一级。当下一级将内存分配给上一级时,都需要判断自己能不能一次性给那么多,否则就要向自己的下一级申请内存。分配内存首先要取出,其次是切分,并且要将最后一个置空
了解了基数树可以实现无锁或最小化锁,从而有效处理并发。
单例模式。
解除头文件循环引用,进一步了解了 C++编译的流程。
初步学习了如何调试多线程程序。