一.项目介绍
1.这个项目做的是什么?
本项目旨在实现一个高并发内存池,其原型是 Google 的开源项目 "tcmalloc"。tcmalloc 全称 Thread-Caching Malloc,即线程缓存的 malloc,它实现了高效的多线程内存管理,用于替代系统的内存分配相关函数(如 malloc、free)。本项目将 tcmalloc 最核心的框架简化后提取出来,模拟实现一个自己的高并发内存池,目的是学习 tcmalloc 的精华部分,而非完全照搬。
tcamlloc源码: tcmalloc: TCMalloc (google-perftools) 是用于优化C++写的多线程应用,比glibc 2.3的malloc快。这个模块可以用来让MySQL在高并发下内存占用更加稳定。
https://gitee.com/mirrors/tcmalloc
2.这个项目的要求的知识储备
这个项目会用到C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等等方面的知识。
2.1什么是内存池
"池化技术"是一种通过预先向系统申请并维护一定数量资源,以提升程序运行效率的优化策略。由于某些资源的申请和释放过程开销较大,频繁操作会影响性能,因此池化技术会在初始化阶段提前获取超过当前需要的资源量,并统一管理,形成资源池。当程序需要使用资源时,直接从池中获取,用完后归还,避免重复申请和释放的系统开销,从而显著提高响应速度和处理效率。
在计算机系统中,池化技术应用广泛,除了内存池之外,还包括连接池、线程池、对象池等。以服务器中的线程池为例,其工作机制是:系统预先创建多个线程并使其处于等待状态;当接收到客户端请求时,从线程池中分配一个空闲线程来处理该请求;请求处理完成后,线程并不销毁,而是再次回到池中等待下一次任务,从而避免线程频繁创建与销毁的开销,提升系统整体性能和资源利用率。
2.2.内存池
内存池是一种内存管理优化策略。其核心思路是程序在启动时,预先向操作系统申请一大块连续的内存区域进行统一管理。此后,当程序内部需要分配内存时,并非每次都直接调用系统的内存分配函数,而是由内存池从其预先申请的大块内存中快速划出一部分来满足需求;同样,当程序释放内存时,也并非真正将内存交还给操作系统,而是将其标记为空闲并回收到内存池中,供后续分配使用。直到程序运行结束或到达特定时机(如内存池销毁时),才会将最初申请的那一整块内存整体归还给操作系统。
这种机制通过减少程序运行期间频繁向操作系统申请和释放内存的次数,有效避免了系统调用带来的开销和可能产生的内存碎片,从而显著提升了内存分配与回收的效率,并使得程序的内存使用行为更加稳定和可控。
2.3.内存池主要解决的问题
内存池的设计主要旨在解决两个核心问题:分配效率 和内存碎片。
- 提升分配效率
这是内存池最直接的目标。通过预先向操作系统批量申请一大块内存并自行管理,程序在运行时的内存分配与释放操作,就由原本昂贵的系统调用,转变为在用户态内存块中进行简单的指针移动或链表操作,从而大幅降低了每次分配的开销。
2. 缓解内存碎片
这是从系统内存分配器视角看的重要优势。内存碎片分为两种类型:
-
外部碎片 :指系统中存在许多分散的、不连续的小块空闲内存。尽管这些空闲内存的总量可能足够大,但由于它们彼此不连续,无法合并成一个足以满足较大分配请求的连续空间,从而导致分配失败。内存池通过 "批发" 一大块连续内存并"零售"给程序使用,在池子内部进行分配管理,可以有效隔离程序内部的分配模式,减少对系统全局内存布局的冲击,从而显著降低外部碎片的产生。
-
内部碎片 :指分配器为了满足内存对齐等要求,实际分配给程序的内存块尺寸略大于其请求的大小。这多出来的、已被分配但无法被程序实际使用的内存空间,就是内部碎片。内部碎片的产生与分配器的实现策略(如固定大小块、对齐值)紧密相关。在我们的后续项目中,将会具体观察到内存池实现中内部碎片的成因和表现,届时可以更直观地理解这一概念。
总而言之,内存池通过在用户空间建立一层中间管理层,以空间(预先占用内存)换时间(分配速度),并优化内存布局,从而系统性地改善了内存使用的性能与健康度。

2.4.malloc
在C/C++中,我们通常使用 "malloc"来动态申请内存,但这并不意味着每次调用 `malloc` 都会直接从操作系统的堆中获取内存。实际上,"malloc" 本身就是一个高度优化、功能完整的内存池管理器。
我们可以把它理解为一个高效的"内存批发零售商":它首先向操作系统"批发"一大块连续的内存区域作为库存。当程序需要分配内存时,"malloc" 并不是每次都去向操作系统"进货",而是优先从其内部管理的"库存"(即内存池)中"零售"出所需大小的内存块给程序使用。只有当其内部库存无法满足需求(例如内存不足,或遇到了一个特别大的分配请求)时,它才会再次向操作系统申请扩充库存。同样,当程序调用 "free"释放内存时,内存通常只是被回收到 "malloc"的内部池中,以备后续重用,而非立即归还给操作系统。
不同平台和编译器环境下的 "malloc" 实现各不相同,它们都是经过深度优化的独立内存分配器。例如,Windows平台上Visual Studio使用的是微软自行实现的分配器;而在Linux系统下,GCC套件通常依赖glibc库中的 ptmalloc(pthreads malloc) 作为其默认分配器。这些分配器虽然核心思想相通,但在具体管理策略、多线程支持、性能和碎片处理上各有特点。
了解这些底层分配器(如ptmalloc)的设计,对于深入理解内存管理、性能调优和系统编程非常有帮助。在完成我们当前的内存池项目后,如果你对"malloc"如何高效地处理多线程竞争、减少碎片等细节感兴趣,可以进一步研究ptmalloc等工业级分配器的实现原理。
一文了解,Linux内存管理,malloc、free 实现原理 - 知乎
malloc()背后的实现原理------内存池 - 阿照的日志

2.5先设计⼀个定长的内存池
作为程序员(C/C++)我们知道申请内存使用的是malloc,malloc其实就是⼀个通用的大众货,什么场景下都可以用,但是什么场景下都可以用就意味着什么场景下都不会有很高的性能,下面我们就先来设计⼀个定长内存池做个开胃菜,当然这个定长内存池在我们后面的高并发内存池中也是有价值的,所以学习它有两层目的,先熟悉⼀下简单内存池是如何控制的,第二它会作为我们后面内存池的⼀个基础组件。




ObjectPool.h
cpp
#pragma once
#include <windows.h>
#include <iostream>
#include <vector>
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
//Linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
template<class T>
class ObjectPool
{
private:
char* _memory = nullptr;//指向大块内存的指针
size_t _remainBytes = 0;//大块内存在切分过程中剩余的字节数
void* _freeList = nullptr;//还回来的链接的自由链表的头指针
public:
T* New()
{
T* obj = nullptr;
//优先把换回来的内存块对象再次重新利用
if (_freeList)
{
//头删_freeList还回来的内存块 用内存块的前4个或者8个字节作头链接
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else
{
//剩余内存不足一个对象大小时,则重新开大块空间
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
_memory = (char*)SystemAlloc(_remainBytes >> 13);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//剩余内存足一个对象大小时
obj = (T*)_memory;
//如果对象大小小于指针的大小就返回指针的大小(32位4字节 64位8字节)否则返回原对象大小
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
//_memory往后移动
_memory += objSize;
//_remainBytes更新结果
_remainBytes -= objSize;
}
//定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//显示调用析构函数清理对象
obj->~T();
//头插放入_freeList中
*(void**)obj = _freeList;
_freeList = obj;
}
};
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
, _left(nullptr)
, _right(nullptr)
{
}
};
void TestObjectPool()
{
// 申请释放的轮次
const size_t Rounds = 5;
// 每轮申请释放多少次
const size_t N = 10000000;
std::vector<TreeNode*> v1;
v1.reserve(N);
size_t begin1 = clock();
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();
std::vector<TreeNode*> v2;
v2.reserve(N);
ObjectPool<TreeNode> TNPool;
size_t begin2 = clock();
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();
std::cout << "new cost time:" << end1 - begin1 << std::endl;
std::cout << "object pool cost time:" << end2 - begin2 << std::endl;
}
unitTest.cpp
cpp
#include "ObjectPool.h"
int main()
{
TestObjectPool();
return 0;
}

二.高并发内存池整体框架设计
现代很多的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题 。malloc本⾝其实已经很优秀,那么我们项目的原型tcmalloc就是在多线程**⾼并发的场景下**更胜⼀筹,所以这次我们实现的内存池需要考虑以下几方面的问题:
1. 性能问题。 2. 多线程环境下,锁竞争问题。 3. 内存碎片问题。
concurrent memory pool主要由以下3个部分构成:
1. 线程缓存(Thread Cache)
线程缓存是每个线程独享的内存缓存层,专门负责分配小于256KB的小内存块。由于每个线程都拥有自己独立的缓存,因此线程在此层级申请内存时无需加锁,极大地提高了高频小内存分配的效率,这是整个高并发内存池性能优异的关键设计。
2. 中心缓存(Central Cache)
中心缓存是所有线程共享的一层缓存,作为线程缓存的上游供给源。 当线程缓存中的内存不足时,会以批次为单位向中心缓存申请补充。同时,中心缓存也会在适当时机(例如线程缓存空闲对象过多时)回收部分内存,以实现不同线程之间的内存负载均衡,避免出现单个线程过度占用内存资源的情况。中心缓存存在线程竞争,因此访问时需要加锁,但通过桶锁(每个内存大小规格独立加锁) 以及低频的交互(仅在线程缓存不足时访问),锁竞争的激烈程度得以有效控制。
3. 页缓存(Page Cache)
页缓存是更底层的缓存,以系统内存页为单位进行内存的管理与分配。当中心缓存需要补充内存时,页缓存会分配若干连续的页(组成一个Span)给中心缓存,由中心缓存将其进一步切割成特定大小的小块内存对象。页缓存的核心作用之一是合并物理相邻的空闲页 。当中心缓存中某个Span的所有内存块都被归还后,页缓存会回收该Span,并尝试将其与相邻的空闲页合并,形成更大的连续内存空间,从而有效减少外部内存碎片,提升大块连续内存的可用性。
这种三层递进式结构(Thread Cache → Central Cache → Page Cache)共同构成了一个兼顾线程局部效率 与全局内存均衡的高并发内存管理体系。

三.Thread cache
thread cache是哈希桶结构,每个桶是⼀个按桶位置映射大小的内存块对象的自由链表。每个线程都会有⼀个thread cache对象,这样每个线程在这⾥获取对象和释放对象时是⽆锁的。

1.FreeList



cpp
//管理切分好的小对象的自由链表
class FreeList
{
private:
void* _freeList = nullptr;
size_t _maxSize = 1;
size_t _size = 0;
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;
_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往后移动
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;
}
};
2.申请内存
- 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
- 如果⾃由链表_freeLists[i]中有对象,则直接Pop⼀个内存对象返回。
- 如果_freeLists[i]中没有对象时,则批量从central cache中获取⼀定数量的对象,插入到自由链表并返回⼀个对象。
cpp
void* ThreadCache::Allocate(size_t size)
{
//size 必须小于 256kb
assert(size <= MAX_BYTES);
//对齐大小
size_t alignSize = SizeClass::RoundUp(size);
//对应哪个桶
size_t index = SizeClass::Index(size);
//如果⾃由链表_freeLists[i]中有对象,则直接Pop⼀个内存对象返回
if (!_freeLists[index].Empty())
{
return _freeLists[index].Pop();
}
//如果_freeLists[i]中没有对象时,则批量从central cache中获取⼀定数量的对象,插⼊到⾃由链表并返回⼀个对象(当对象释放内存时候才会放入)
else
{
return FetchFromCentralCache(index, alignSize);
}
}
3.释放内存
- 当释放内存小于256kb时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
- 当链表的长度过长,则回收⼀部分内存对象到central cache。
cpp
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
//找到对应映射的_freeList[index],将对象插入_freeList
size_t index = SizeClass::Index(size);
_freeLists[index].Push(ptr);
//当链表长度大于一次批量申请的内存时候就开始还一段区间的对象给 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;
list.PopRange(start, end, list.MaxSize());
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
4.利用TLS------thread local storage:
TLS 是一种机制,是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保证了数据的线程独立性。
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
用法详细介绍:Thread Local Storage(线程局部存储)TLS - 知乎
1.ConcurrentAlloc

2.ConcurrentFree

四.Central Cache
central cache也是⼀个哈希桶结构,它的哈希桶的映射关系跟thread cache是⼀样的。不同的是它的每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下⾯的span中的大内存块被按映射关系切成了⼀个个小内存块对象挂在span的自由链表中。

1.Span
cpp
//管理多个连续页大块内存跨度结构
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; //是否在被使用
};
2.SpanList
cpp
// 带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
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;
}
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);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx; // 桶锁
};
3.申请内存
1.当 thread cache 的内存不足时,它会按照一定的数量规则,向 central cache 批量申请内存对象。其中,批量申请的数量采用了类似 TCP 慢启动的策略进行控制。在 central cache 中,内存是通过一个哈希结构管理的,每个哈希桶对应一个 span 链表(spanlist),每个 span 管理着一批连续的内存块。当 central cache 向 thread cache 分配内存对象时,会从对应的 span 中取出所需对象。为了确保并发安全,这个过程需要对相应的哈希桶加锁,但这种锁是以桶为粒度设计的,可以有效减少锁竞争,提升整体性能。
3.1慢启动的策略
cpp
//一次给 thread cache 多少个内存块
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
//[2, 512] 一次批量移动多少个对象的(慢启动)上限值
//小对象一次批量上限高
//大对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
2.当某个 central cache 的 spanlist 中所有的 span 都没有可供分配的内存对象时,central cache 会向 page cache 申请一个新的 span。在获得这个新的 span 之后,central cache 会将它所管理的大块内存按照特定大小进行划分,并将这些划分出来的内存块组织成自由链表(free list)。最后,central cache 便可以从这个新准备的自由链表中取出内存对象,分配给之前发出请求的 thread cache。
3.2 GetOneSpan
cpp
//获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//首先查看当前 spanlist 中是否还有未分配对象的 span
//遍历当前spanlist
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
// 如果 span 当前的 _freeList != nullptr
// 返回当前 span
return it;
}
else
{
//继续往后遍历
it = it->_next;
}
}
//当前 spanlist 中没有空闲的 span , 只能找 Page Cache 申请
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//PageCache NewSpan
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//span->_isUse 设置为 true
span->_isUse = true;
//记录 span->_objSize
span->_objSize = size;
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
//对获取的 span 进行切分 (不需要加锁,因为其他线程不会访问到这个span)
//计算 span 的大块内存的起始地址
char* start = (char*)(span->_pageId << PAGE_SHIFT);
//计算 span 的大块内存的大小(字节数)
size_t bytes = span->_n << PAGE_SHIFT;
//计算 span 的大块内存的结束地址
char* end = start + bytes;
//把大块内存切成自由链表链接起来
//1.先切一块下来做头,方便尾插
span->_freeList = start;
start += size;
void* tail = span->_freeList;
int i = 1;//调试观察切了多少块
while (start < end)
{
++i;
NextObj(tail) = start;
tail = NextObj(tail);
start += size;
}
NextObj(tail) = nullptr;
list.PushFront(span);
//返回 span
return span;
}
3.3 FetchRangeObj to the thread cache
cpp
//给一定数量的对象分配给 thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
//计算映射位置
size_t index = SizeClass::Index(size);
//加锁获得一个span
_spanLists[index]._mtx.lock();
Span* span = GetOneSpan(_spanLists[index], size);
//断言span 和 span->_freeList 一定存在
assert(span);
assert(span->_freeList);
//从 span 中获取 batchNum 个对象
//如果不够 batchNum个,有多少拿多少
start = span->_freeList;
end = start;
size_t i = 0;
//定义actualNum
size_t actualNum = 1;
//循环取对象
while (i < batchNum - 1 && NextObj(end) != nullptr)
{
end = NextObj(end);
++i;
++actualNum;
}
//循环结束后 end 的 next 作为 span->_freeList 新的头
span->_freeList = NextObj(end);
//把 end 的 next 置成空
NextObj(end) = nullptr;
//span->_useCount增加
span->_useCount += actualNum;
//_spanLists 该位置解锁
_spanLists[index]._mtx.unlock();
//返回 actualNum
return actualNum;
}
- central cache的中挂的span中use_count记录分配了多少个对象出去,分配⼀个对象给thread
cache,就++use_count。


4.释放内存
- 当 thread_cache 过长或者线程销毁,则会将内存释放回 central_cache 中。释放回来时--use_count,当 use_count 减到 0 时则表示所有对象都回到了 span,则将 span 释放回 page_cache,page_cache 中会对前后相邻的空闲页进行合并。
五.Page Cache
1.申请内存
-
当 central cache 向 page cache 申请内存时,page cache 先检查对应位置有没有 span,如果没有则向更大页寻找一个 span,如果找到则分裂成两个。比如:申请的是 4 页 page,4 页 page 后面没有挂 span,则向后面寻找更大的 span,假设在 10 页 page 位置找到一个 span,则将 10 页 page span 分裂为一个 4 页 page span 和一个 6 页 page span。
-
如果找到 _spanList[128] 都没有合适的 span,则向系统使用 mmap、brk 或者是 VirtualAlloc 等方式申请 128 页 page span 挂在自由链表中,再重复 1 中的过程。
-
需要注意的是 central cache 和 page cache 的核心结构都是 spanlist 的哈希桶,但是他们是有本质区别的,central cache 中哈希桶,是按跟 thread cache 一样的大小对齐关系映射的,它的 spanlist 中挂的 span 中的内存都被按映射关系切好链接成小块内存的自由链表。而 page cache 中的 spanlist 则是按下标桶号映射的,也就是说第 i 号桶中挂的 span 都是 i 页内存。




1.1 NewSpan
cpp
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
//先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
//如果有,弹出_spanLists[k]
Span* kSpan = _spanLists[k].PopFront();
return kSpan;
}
//遍历整个桶 , 检查后面的桶里面有没有 span ,如果有可以把它进行切分
for (size_t i = k + 1; i < NPAGES; ++i)
{
if (!_spanLists[i].Empty())
{
//如果有 , 弹出 _spanLists[i]
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
//在 nSpan 的头部切 k 页下来
// k 页 span 返回
// nSpan 再挂到对应的映射位置
//改变 kSpan->_pageId 和 kSpan->_n
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;
//将 bigSpan 推入 _spanLists 中
_spanLists[bigSpan->_n].PushFront(bigSpan);
//递归 NewSpan
return NewSpan(k);
}
1.2 申请流程全过程调试测试

2.释放内存
2.1 TestAddressShift


2.2 ReleaseSpanToPageCache
- 如果 central cache 释放回一个 span,则依次寻找 span 的前后 page_id 的没有在使用的空闲 span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的 span,减少内存碎片。


六.细节优化
1.申请细节优化
规定 : size

1.1 ConcurrentAlloc 优化
cpp
// ConcurrentAlloc
static void* ConcurrentAlloc(size_t size)
{
// ⼤于 MAX_BYTES ( MAX_BYTES = 256 * 1024 )就直接找 pagecache 或者系统堆申请
if (size > MAX_BYTES)
{
//计算对齐大小
size_t alignSize = SizeClass::RoundUp(size);
//计算一共要几页 alignSize >> PAGE_SHIFT
size_t kpage = alignSize >> PAGE_SHIFT;
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//New k 页的 span
Span* span = PageCache::GetInstance()->NewSpan(kpage);
//记录对象大小
span->_objSize = size;
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
//获取span->_pageId之后转换为地址
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
//返回ptr
return ptr;
}
else
{
//通过 TLS 每个线程无锁的获取自己专属的ThreadCache对象
//如果pTLSThreadCache为nullptr,先创建
if (pTLSThreadCache == nullptr)
{
static ObjectPool<ThreadCache> tcPool;
//pTLSThreadCache = new ThreadCache
pTLSThreadCache = tcPool.New();
}
return pTLSThreadCache->Allocate(size);
}
}
1.2 NewSpan 优化
cpp
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
//大于128 page的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;
//优化 -- 使用定长内存池申请
Span* span = _spanPool.New();
//计算页号
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
//设置页数
span->_n = k;
//建立 _pageId 与 span 的映射关系 , 方便回收
_idSpanMap[span->_pageId] = span;
return span;
}
//先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
//如果有,弹出_spanLists[k]
Span* kSpan = _spanLists[k].PopFront();
//建立 _pageId 与 span 的映射关系 ,方便 central cache 回收小块内存时候,查找对应的 span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
//遍历整个桶 , 检查后面的桶里面有没有 span ,如果有可以把它进行切分
for (size_t i = k + 1; i < NPAGES; ++i)
{
if (!_spanLists[i].Empty())
{
//如果有 , 弹出 _spanLists[i]
Span* nSpan = _spanLists[i].PopFront();
//Span* kSpan = new Span;
//优化
Span* kSpan = _spanPool.New();
//在 nSpan 的头部切 k 页下来
// k 页 span 返回
// nSpan 再挂到对应的映射位置
//改变 kSpan->_pageId 和 kSpan->_n
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
_spanLists[nSpan->_n].PushFront(nSpan);
//建立 nSpan 的页号和 nSpan 映射,方便 page cache 回收内存时进行合并查找
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 建立 kSpan->_pageId 和 kSpan 的映射,方便 central cache 回收小块内存的时候,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
}
//走到这个位置说明没有大页的span了
//这时候就要去找堆要一个128页的span
//Span* bigSpan = new Span;
Span* bigSpan = _spanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
//将 bigSpan 推入 _spanLists 中
_spanLists[bigSpan->_n].PushFront(bigSpan);
//递归NewSpan
return NewSpan(k);
}
2.释放细节优化
2.1 ConcurrentFree 优化
cpp
static void ConcurrentFree(void* ptr)
{
//根据 ptr 计算应该还给哪个 span
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
//需要还的大小从 span 所记录的 _objSize 中获取
size_t size = span->_objSize;
//如果 size > MAX_BYTES 交给 PageCache 处理
if (size > MAX_BYTES)
{
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//PageCache ReleaseSpanToPageCache
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
}
//否则调用 pTLSThreadCache 的 Deallocate
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
2.2 ReleaseSpanToPageCache 优化
cpp
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于 128 page 的直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
SystemFree(ptr);
//delete span;
_spanPool.Delete(span);
return;
}
//对 span 前后的页尝试进行合并 , 缓解内存碎片问题
while (1)
{
//对前页尝试合并
PAGE_ID prevId = span->_pageId - 1;
auto ret = _idSpanMap.find(prevId);
//前面的页号没有查找到,不合并了 break
if (ret == _idSpanMap.end())
{
break;
}
//auto ret = (Span*)_idSpanMap.get(prevId);
//if (ret == nullptr)
//{
// break;
//}
//前面页在使用 , 不进行合并
Span* prevSpan = ret->second;
//Span* prevSpan = ret;
if (prevSpan->_isUse == true)
{
break;
}
// 合并超过128页的 span 没办法管理 , 不合并了
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
//改变span的_pageId
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
_spanLists[prevSpan->_n].Erase(prevSpan);
//delete prevSpan
_spanPool.Delete(prevSpan);
}
//向后合并
while (1)
{
//对前页尝试合并
PAGE_ID nextId = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextId);
//前面的页号没有查找到,不合并了 break
if (ret == _idSpanMap.end())
{
break;
}
//auto ret = (Span*)_idSpanMap.get(nextId);
//if (ret == nullptr)
//{
// break;
//}
//后面页在使用 , 不进行合并
Span* nextSpan = ret->second;
//Span* nextSpan = ret;
if (nextSpan->_isUse == true)
{
break;
}
// 合并超过128页的 span 没办法管理 , 不合并了
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
//改变span的_pageId
span->_n += nextSpan->_n;
//从 _spanLists[nextSpan->_n] 删除 nextSpan
_spanLists[nextSpan->_n].Erase(nextSpan);
//delete nextSpan
_spanPool.Delete(nextSpan);
}
//合并完成后 , 把 span 推入 _spanLists 中
_spanLists[span->_n].PushFront(span);
//span 的使用情况设为未使用
span ->_isUse = false;
//建立新 span 和 id 的映射关系
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}
3.多线程并发环境下,对比malloc和ConcurrentAlloc申请和释放内存效率对比
Benchmark.cpp 测试
cpp
#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, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, (size_t)free_costtime);
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + 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, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, (size_t)free_costtime);
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + free_costtime);
}
int main()
{
size_t n = 10000;
// 4个线程 10轮 每轮10000次
BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n,4, 10);
cout << endl << endl;
return 0;
}
测试结果

4.使用tcmalloc源码中实现基数树进行优化
在我们的项目中需要查找 span 和 id 的映射关系 , 所以涉及到很多的查找 , 也存在锁竞争问题 , 通过使用基数树可以通过建立数到指针的映射关系 (指针本质也是数字) 提高查找效率 , 并且查找的时候可以不加锁了。
1.基数树
基数树 ,又称压缩前缀树 或Patricia树,是一种多叉树数据结构,用于高效存储和检索字符串或二进制序列。它是Trie(前缀树)的空间优化版本。

2.原理

3.PageMap.h (从源码中改造的)
cpp
#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)) {
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() {
}
};
4.改造后测试结果对比

七.完整代码
1.Common.h
cpp
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <map>
#include <algorithm>
#include <time.h>
#include <assert.h>
#include <thread>
#include <mutex>
#include <atomic>
using std::cout;
using std::endl;
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/mman.h> // mmap, munmap
#include <unistd.h> // sysconf
#include <stdexcept> // std::bad_alloc
#endif
// ⼩于等于MAX_BYTES就找 threadcache 申请
// ⼤于MAX_BYTES就直接找 pagecache 或者系统堆申请
static const size_t MAX_BYTES = 256 * 1024;
// threadcache 和 centralcache 自由链表哈希桶的表⼤⼩
static const size_t NFREELIST = 208;
// pagecache 管理 spanlist 哈希表⼤⼩
static const size_t NPAGES = 129;
// ⻚⼤⼩转换偏移 即⼀⻚定义为2^13 , 也就是8KB
static const size_t PAGE_SHIFT = 13;
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#else
typedef uintptr_t PAGE_ID;
#endif
//直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (ptr == nullptr) {
throw std::bad_alloc();
}
return ptr;
#else
// Linux实现
// 获取系统页大小
long system_page_size = sysconf(_SC_PAGESIZE);
if (system_page_size == -1) {
// 默认使用4KB
system_page_size = 4096;
}
// 计算需要申请的内存大小
size_t size = kpage * system_page_size;
// 使用mmap申请匿名内存(类似VirtualAlloc)
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,-1,0);
if (ptr == MAP_FAILED) {
throw std::bad_alloc();
}
return ptr;
#endif
}
inline static void SystemFree(void* ptr) {
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// Linux下使用 munmap 释放
if (ptr != nullptr) {
long system_page_size = sysconf(_SC_PAGESIZE);
if (system_page_size == -1) {
system_page_size = 4096;
}
size_t kpage = (PAGE_ID)ptr >> PAGE_SHIFT;
size_t size = kpage * system_page_size;
munmap(ptr, size);
}
#endif
}
//获取内存对象中存储的头4 or 8字节值,链接的下⼀个对象的地址
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
private:
void* _freeList = nullptr;
size_t _maxSize = 1;
size_t _size = 0;
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;
_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往后移动
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;
}
};
// 计算对象大小的对齐映射规则
class SizeClass
{
public:
// 整体控制在最多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)
/*size_t _RoundUp(size_t size, size_t alignNum)
{
size_t alignSize;
if (size % alignNum != 0)
{
alignSize = (size / alignNum + 1)*alignNum;
}
else
{
alignSize = size;
}
return alignSize;
}*/
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return ((bytes + alignNum - 1) & ~(alignNum - 1));
}
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);//8字节
}
else if (size <= 1024)
{
return _RoundUp(size, 16);//16字节
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);//128字节
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);//1024字节
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);//1 KB
}
else
{
return _RoundUp(size, 1 << PAGE_SHIFT);//8KB
}
}
/*size_t _Index(size_t bytes, size_t alignNum)
{
if (bytes % alignNum == 0)
{
return bytes / alignNum - 1;
}
else
{
return bytes / alignNum;
}
}*/
// 整体控制在最多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)
// 1 + 7 8
// 2 9
// ...
// 8 15
// 9 + 7 16
// 10
// ...
// 16 23
static inline size_t _Index(size_t bytes, size_t align_shift)
{
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); //2^3 8
}
else if (bytes <= 1024) {
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 8 * 1024) {
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024) {
return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
}
else if (bytes <= 256 * 1024) {
return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else {
assert(false);
}
return -1;
}
//一次给 thread cache 多少个内存块
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
//[2, 512] 一次批量移动多少个对象的(慢启动)上限值
//小对象一次批量上限高
//大对象一次批量上限低
int num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
//计算一次向系统获取几个页
static size_t NumMovePage(size_t size)
{
//一次给 thread cache 多少个内存块
size_t num = NumMoveSize(size);
size_t npage = num * size;
//小于一页给一页(8kb)
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
//管理多个连续页大块内存跨度结构
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; //是否在被使用
};
// 带头双向循环链表
class SpanList
{
private:
Span* _head;
public:
std::mutex _mtx; // 桶锁
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
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;
}
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);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
};
2.ConcurrentAlloc.h
cpp
#pragma once
#include "Common.h"
#include "ThreadCache.h"
#include "PageCache.h"
#include "ObjectPool.h"
// ConcurrentAlloc
static void* ConcurrentAlloc(size_t size)
{
// ⼤于 MAX_BYTES ( MAX_BYTES = 256 * 1024 )就直接找 pagecache 或者系统堆申请
if (size > MAX_BYTES)
{
//计算对齐大小
size_t alignSize = SizeClass::RoundUp(size);
//计算一共要几页 alignSize >> PAGE_SHIFT
size_t kpage = alignSize >> PAGE_SHIFT;
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//New k 页的 span
Span* span = PageCache::GetInstance()->NewSpan(kpage);
//记录对象大小
span->_objSize = size;
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
//获取span->_pageId之后转换为地址
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
//返回ptr
return ptr;
}
else
{
//通过 TLS 每个线程无锁的获取自己专属的ThreadCache对象
//如果pTLSThreadCache为nullptr,先创建
if (pTLSThreadCache == nullptr)
{
static ObjectPool<ThreadCache> tcPool;
//pTLSThreadCache = new ThreadCache
pTLSThreadCache = tcPool.New();
}
return pTLSThreadCache->Allocate(size);
}
}
static void ConcurrentFree(void* ptr)
{
//根据 ptr 计算应该还给哪个 span
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
//需要还的大小从 span 所记录的 _objSize 中获取
size_t size = span->_objSize;
//如果 size > MAX_BYTES 交给 PageCache 处理
if (size > MAX_BYTES)
{
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//PageCache ReleaseSpanToPageCache
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
}
//否则调用 pTLSThreadCache 的 Deallocate
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
3.ObjectPool.h
cpp
#pragma once
#include "Common.h"
template<class T>
class ObjectPool
{
private:
char* _memory = nullptr;//指向大块内存的指针
size_t _remainBytes = 0;//大块内存在切分过程中剩余的字节数
void* _freeList = nullptr;//还回来的链接的自由链表的头指针
public:
T* New()
{
T* obj = nullptr;
//优先把换回来的内存块对象再次重新利用
if (_freeList)
{
//头删_freeList还回来的内存块 用内存块的前4个或者8个字节作头链接
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
else
{
//剩余内存不足一个对象大小时,则重新开大块空间
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;
_memory = (char*)SystemAlloc(_remainBytes >> 13);
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//剩余内存足一个对象大小时
obj = (T*)_memory;
//如果对象大小小于指针的大小就返回指针的大小(32位4字节 64位8字节)否则返回原对象大小
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
//_memory往后移动
_memory += objSize;
//_remainBytes更新结果
_remainBytes -= objSize;
}
//定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//显示调用析构函数清理对象
obj->~T();
//头插放入_freeList中
*(void**)obj = _freeList;
_freeList = obj;
}
};
struct TreeNode
{
int _val;
TreeNode* _left;
TreeNode* _right;
TreeNode()
:_val(0)
, _left(nullptr)
, _right(nullptr)
{
}
};
//void TestObjectPool()
//{
// // 申请释放的轮次
// const size_t Rounds = 5;
//
// // 每轮申请释放多少次
// const size_t N = 10000000;
//
// std::vector<TreeNode*> v1;
// v1.reserve(N);
//
// size_t begin1 = clock();
// 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();
//
// std::vector<TreeNode*> v2;
// v2.reserve(N);
//
// ObjectPool<TreeNode> TNPool;
// size_t begin2 = clock();
// 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();
//
// std::cout << "new cost time:" << end1 - begin1 << std::endl;
// std::cout << "object pool cost time:" << end2 - begin2 << std::endl;
//}
4.PageMap.h
cpp
#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)) {
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() {
}
};
5.ThreadCache.h
cpp
#pragma once
#include "Common.h"
class ThreadCache
{
private:
FreeList _freeLists[NFREELIST];
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
//从Central Cache中获取内存对象
void* FetchFromCentralCache(size_t index, size_t size);
//释放对象时,链表过长,回收内存到Central Cache
void ListTooLong(FreeList& list, size_t size);
};
//TLS thread local storage windows下 其他系统实现不一样
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
6.ThreadCache.cpp
cpp
#include "ThreadCache.h"
#include "CentralCache.h"
//Thread Cache 是每个线程独享的内存缓存层,专门负责分配小于256KB的小内存块
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(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;
}
void* start = nullptr;
void* end = nullptr;
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum > 0);
if (actualNum == 1)
{
assert(start == end);
return start;
}
else
{
_freeLists[index].PushRange(NextObj(start), end, actualNum - 1);
return start;
}
}
void* ThreadCache::Allocate(size_t size)
{
//size 必须小于 256kb
assert(size <= MAX_BYTES);
//对齐大小
size_t alignSize = SizeClass::RoundUp(size);
//对应哪个桶
size_t index = SizeClass::Index(size);
//如果⾃由链表_freeLists[i]中有对象,则直接Pop⼀个内存对象返回(当对象释放内存时候才会放入)
if (!_freeLists[index].Empty())
{
return _freeLists[index].Pop();
}
//如果_freeLists[i]中没有对象时,则批量从central cache中获取⼀定数量的对象,插⼊到⾃由链表并返回⼀个对象
else
{
return FetchFromCentralCache(index, alignSize);
}
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
//找到对应映射的_freeList[index],将对象插入_freeList
size_t index = SizeClass::Index(size);
_freeLists[index].Push(ptr);
//当链表长度大于一次批量申请的内存时候就开始还一段区间的对象给 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;
list.PopRange(start, end, list.MaxSize());
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
7.CentralCache.h
cpp
#pragma once
#include "Common.h"
//Central Cache 设计为单例模式
class CentralCache
{
private:
SpanList _spanLists[NFREELIST];
private:
CentralCache(){}
CentralCache(const CentralCache&) = delete;
static CentralCache _sInst;
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
//获取一个非空的 span
Span* GetOneSpan(SpanList& list, size_t btye_size);
//给一定数量的对象分配给 thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
//将一定数量的对象释放到 spanlist 的 span中
void ReleaseListToSpans(void* start, size_t byte_size);
};
8.CentralCache.cpp
cpp
#include "CentralCache.h"
#include "PageCache.h"
CentralCache CentralCache::_sInst;
//获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//首先查看当前 spanlist 中是否还有未分配对象的 span
//遍历当前spanlist
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
// 如果 span 当前的 _freeList != nullptr
// 返回当前 span
return it;
}
else
{
//继续往后遍历
it = it->_next;
}
}
//注意先把 central cache 的桶锁解掉,这样如果其他线程释放内存对象回来不会发生阻塞
list._mtx.unlock();
//当前 spanlist 中没有空闲的 span , 只能找 Page Cache 申请
//PageCache 加锁
PageCache::GetInstance()->_pageMtx.lock();
//PageCache NewSpan
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//span->_isUse 设置为 true
span->_isUse = true;
//记录 span->_objSize
span->_objSize = size;
//PageCache 解锁
PageCache::GetInstance()->_pageMtx.unlock();
//对获取的 span 进行切分 (不需要加锁,因为其他线程不会访问到这个span)
//计算 span 的大块内存的起始地址
char* start = (char*)(span->_pageId << PAGE_SHIFT);
//计算 span 的大块内存的大小(字节数)
size_t bytes = span->_n << PAGE_SHIFT;
//计算 span 的大块内存的结束地址
char* end = start + bytes;
//把大块内存切成自由链表链接起来
//1.先切一块下来做头,方便尾插
span->_freeList = start;
start += size;
void* tail = span->_freeList;
int i = 1;//调试观察切了多少块
while (start < end)
{
++i;
NextObj(tail) = start;
tail = NextObj(tail);
start += size;
}
NextObj(tail) = nullptr;
//条件断点
int j = 0;
void* cur = span->_freeList;
while (cur)
{
cur = NextObj(cur);
++j;
}
if (j != (bytes / size))
{
int x = 0;
}
//切好 span 以后,需要把 span 挂到桶里面去的时候再加锁
list._mtx.lock();
list.PushFront(span);
//返回 span
return span;
}
//给一定数量的对象分配给 thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
//计算映射位置
size_t index = SizeClass::Index(size);
//加锁获得一个span
_spanLists[index]._mtx.lock();
Span* span = GetOneSpan(_spanLists[index], size);
//断言span 和 span->_freeList 一定存在
assert(span);
assert(span->_freeList);
//从 span 中获取 batchNum 个对象
//如果不够 batchNum个,有多少拿多少
start = span->_freeList;
end = start;
size_t i = 0;
//定义actualNum
size_t actualNum = 1;
//循环取对象
while (i < batchNum - 1 && NextObj(end) != nullptr)
{
end = NextObj(end);
++i;
++actualNum;
}
//循环结束后 end 的 next 作为 span->_freeList 新的头
span->_freeList = NextObj(end);
//把 end 的 next 置成空
NextObj(end) = nullptr;
//span->_useCount增加
span->_useCount += actualNum;
//// 条件断点
//int j = 0;
//void* cur = start;
//while (cur)
//{
// cur = NextObj(cur);
// ++j;
//}
//if (j != actualNum)
//{
// int x = 0;
//}
//_spanLists 该位置解锁
_spanLists[index]._mtx.unlock();
//返回actualNum
return actualNum;
}
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
//合并为一个span
size_t index = SizeClass::Index(size);
//_spanLists[index] 加锁
_spanLists[index]._mtx.lock();
//循环取一个一个的内存对象
while (start)
{
//获取start的next
void* next = NextObj(start);
//从起始地址计算映射关系获取span
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
//span的头赋值给NextObj(start)
NextObj(start) = span->_freeList;
//start 赋值给 span->_freeList
span->_freeList = start;
//span->_useCount--
span->_useCount--;
//当 span->_useCount == 0 时候说明span切分出去的所有小块内存都回来了
//这个 span 就可以再尝试回收给 page cache , page cache 可以去尝试做前后页的合并
if (span->_useCount == 0)
{
//将 span 从这个_spanLists[index] 下删除
_spanLists[index].Erase(span);
//将 span 的 _freeList _next _prev 置空
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
//释放 span 给 page cache 时候,使用 page cache 的锁就可以了
//把桶锁解掉
_spanLists[index]._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
//调用 page cache 的 ReleaseSpanToPageCache
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
//再把桶锁加上
_spanLists[index]._mtx.lock();
}
start = next;
}
//再解桶锁
_spanLists[index]._mtx.unlock();
}
9.PageCache.h
cpp
#pragma once
#include "Common.h"
#include "ObjectPool.h"
#include "PageMap.h"
class PageCache
{
private:
SpanList _spanLists[NPAGES];
ObjectPool<Span> _spanPool;
//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
//std::map<PAGE_ID, Span*> _idSpanMap;
TCMalloc_PageMap2<32 - PAGE_SHIFT> _idSpanMap;
private:
PageCache(){}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//获取一个 K 页的 span
Span* NewSpan(size_t k);
//获取从对象到 span 的映射
Span* MapObjectToSpan(void* obj);
//释放空闲 span 回到 PageCache , 并合并相邻的 span
void ReleaseSpanToPageCache(Span* span);
std::mutex _pageMtx;
};
10.PageCache.cpp
cpp
#include "PageCache.h"
PageCache PageCache::_sInst;
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
//大于128 page的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;
//优化 -- 使用定长内存池申请
Span* span = _spanPool.New();
//计算页号
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
//设置页数
span->_n = k;
//建立 _pageId 与 span 的映射关系 , 方便回收
//_idSpanMap[span->_pageId] = span;
// 优化 -- 建立 _pageId 与 span 的映射关系
_idSpanMap.set(span->_pageId, span);
return span;
}
//先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
//如果有,弹出_spanLists[k]
Span* kSpan = _spanLists[k].PopFront();
//建立 _pageId 与 span 的映射关系 ,方便 central cache 回收小块内存时候,查找对应的 span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
//_idSpanMap[kSpan->_pageId + i] = kSpan;
_idSpanMap.set(kSpan->_pageId + i, kSpan);
}
return kSpan;
}
//遍历整个桶 , 检查后面的桶里面有没有 span ,如果有可以把它进行切分
for (size_t i = k + 1; i < NPAGES; ++i)
{
if (!_spanLists[i].Empty())
{
//如果有 , 弹出 _spanLists[i]
Span* nSpan = _spanLists[i].PopFront();
//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();
//在 nSpan 的头部切 k 页下来 k 页 span 返回 nSpan 再挂到对应的映射位置
//改变 kSpan->_pageId 和 kSpan->_n
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
//把 nspan 放入 _spanLists
_spanLists[nSpan->_n].PushFront(nSpan);
//建立 nSpan 的页号和 nSpan 映射,方便 page cache 回收内存时进行合并查找
//_idSpanMap[nSpan->_pageId] = nSpan;
//_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
_idSpanMap.set(nSpan->_pageId,nSpan);
_idSpanMap.set(nSpan->_pageId + nSpan->_n - 1,nSpan);
// 建立 kSpan->_pageId 和 kSpan 的映射,方便 central cache 回收小块内存的时候,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
//_idSpanMap[kSpan->_pageId + i] = kSpan;
_idSpanMap.set(kSpan->_pageId + i,kSpan);
}
return kSpan;
}
}
//走到这个位置说明没有大页的span了 这时候就要去找堆要一个128页的span
//Span* bigSpan = new Span;
Span* bigSpan = _spanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
//将 bigSpan 推入 _spanLists 中
_spanLists[bigSpan->_n].PushFront(bigSpan);
//递归NewSpan
return NewSpan(k);
}
Span* PageCache::MapObjectToSpan(void* obj)
{
//利用地址计算PAGE_ID
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
//std::unique_lock<std::mutex> lock(_pageMtx);
//auto ret = _idSpanMap.find(id);
//if (ret != _idSpanMap.end())
//{
// return ret->second;
//}
//else
//{
// assert(false);
// return nullptr;
//}
auto ret = (Span*)_idSpanMap.get(id);
assert(ret != nullptr);
return ret;
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于128 page 的直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
SystemFree(ptr);
_spanPool.Delete(span);
return;
}
//对 span 前后的页尝试进行合并 , 缓解内存碎片问题
while (1)
{
//对前页尝试合并
PAGE_ID prevId = span->_pageId - 1;
//auto ret = _idSpanMap.find(prevId);
////前面的页号没有查找到,不合并了 break
//if (ret == _idSpanMap.end())
//{
// break;
//}
auto ret = (Span*)_idSpanMap.get(prevId);
if (ret == nullptr)
{
break;
}
//前面页在使用 , 不进行合并
//Span* prevSpan = ret->second;
Span* prevSpan = ret;
if (prevSpan->_isUse == true)
{
break;
}
// 合并超过128页的 span 没办法管理 , 不合并了
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
//改变span的_pageId
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
//从 _spanLists 中删除 prevSpan
_spanLists[prevSpan->_n].Erase(prevSpan);
//delete prevSpan
_spanPool.Delete(prevSpan);
}
//向后合并
while (1)
{
//对前页尝试合并
PAGE_ID nextId = span->_pageId + span->_n;
//auto ret = _idSpanMap.find(nextId);
////前面的页号没有查找到,不合并了 break
//if (ret == _idSpanMap.end())
//{
// break;
//}
auto ret = (Span*)_idSpanMap.get(nextId);
if (ret == nullptr)
{
break;
}
//后面页在使用 , 不进行合并
//Span* nextSpan = ret->second;
Span* nextSpan = ret;
if (nextSpan->_isUse == true)
{
break;
}
// 合并超过128页的 span 没办法管理 , 不合并了
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
//改变span的_pageId
span->_n += nextSpan->_n;
//从 _spanLists[nextSpan->_n] 删除 nextSpan
_spanLists[nextSpan->_n].Erase(nextSpan);
//delete nextSpan
_spanPool.Delete(nextSpan);
}
//合并完成后 , 把 新的 span 推入 _spanLists 中
_spanLists[span->_n].PushFront(span);
//span 的使用情况设为未使用
span ->_isUse = false;
//建立新 span 和 id 的映射关系
//_idSpanMap[span->_pageId] = span;
//_idSpanMap[span->_pageId + span->_n - 1] = span;
_idSpanMap.set(span->_pageId, span);
_idSpanMap.set(span->_pageId + span->_n - 1, span);
}
11.Benchmark.cpp
cpp
#include"ConcurrentAlloc.h"
// ntimes 一轮申请和释放内存的次数
// rounds 轮次
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
//std::atomic 是不可拷贝的 直接赋值0可能会报错
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, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, (size_t)free_costtime);
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + 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, (size_t)malloc_costtime);
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, (size_t)free_costtime);
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, (size_t)malloc_costtime + free_costtime);
}
int main()
{
size_t n = 10000;
// 4个线程 10轮 每轮10000次
BenchmarkMalloc(n, 4, 10);
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n,4, 10);
cout << endl << endl;
return 0;
}
12.UnitTest.cpp
cpp
#include "ObjectPool.h"
#include "ConcurrentAlloc.h"
void Alloc1()
{
void* ptr = nullptr;
for (size_t i = 0; i < 5; ++i)
{
void* ptr = ConcurrentAlloc(6);
}
cout << ptr << endl;
}
void Alloc2()
{
void* ptr = nullptr;
for (size_t i = 0; i < 5; ++i)
{
void* ptr = ConcurrentAlloc(7);
}
cout << ptr << endl;
}
void TLSTest()
{
std::thread t1(Alloc1);
t1.join();
std::thread t2(Alloc2);
t2.join();
}
void TestConcurrentAlloc1()
{
void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(7);
void* p5 = ConcurrentAlloc(8);
void* p6 = ConcurrentAlloc(8);
void* p7 = ConcurrentAlloc(8);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
cout << p4 << endl;
cout << p5 << endl;
cout << p6 << endl;
cout << p7 << endl;
ConcurrentFree(p1);
ConcurrentFree(p2);
ConcurrentFree(p3);
ConcurrentFree(p4);
ConcurrentFree(p5);
ConcurrentFree(p6);
ConcurrentFree(p7);
}
void TestConcurrentAlloc2()
{
for (size_t i = 0; i < 1024; ++i)
{
void* p1 = ConcurrentAlloc(6);
cout << p1 << endl;
}
void* p2 = ConcurrentAlloc(8);
cout << p2 << endl;
}
void TestAddressShift()
{
PAGE_ID id1 = 2000;
PAGE_ID id2 = 2001;
char* p1 = (char*)(id1 << PAGE_SHIFT);
char* p2 = (char*)(id2 << PAGE_SHIFT);
while (p1 < p2)
{
cout << (void*)p1 << ":" << ((PAGE_ID)p1 >> PAGE_SHIFT) << endl;
p1 += 8;
}
}
void MultiThreadAlloc1()
{
std::vector<void*> v;
for (size_t i = 0; i < 7; ++i)
{
void* ptr = ConcurrentAlloc(6);
v.push_back(ptr);
}
for (auto e : v)
{
ConcurrentFree(e);
}
}
void MultiThreadAlloc2()
{
std::vector<void*> v;
for (size_t i = 0; i < 7; ++i)
{
void* ptr = ConcurrentAlloc(16);
v.push_back(ptr);
}
for (auto e : v)
{
ConcurrentFree(e);
}
}
void TestMultiThread()
{
std::thread t1(MultiThreadAlloc1);
std::thread t2(MultiThreadAlloc2);
t1.join();
t2.join();
}
void BigAlloc()
{
void* p1 = ConcurrentAlloc(257 * 1024);
ConcurrentFree(p1);
cout << p1 << endl;
void* p2 = ConcurrentAlloc(129 * 8 * 1024);
ConcurrentFree(p2);
cout << p2 << endl;
}
int main()
{
//TestObjectPool();
//TLSTest();
// TestConcurrentAlloc1();
// TestConcurrentAlloc2();
//TestAddressShift();
//TestMultiThread();
//BigAlloc();
return 0;
}
八.扩展学习及当前项目实现的不足
实际中我们测试了,当前实现的并发内存池比malloc/free是更加高效的,那么我们能否替换到系统
调⽤malloc呢?实际上是可以的。
• 不同平台替换⽅式不同。 基于unix的系统上的glibc,使用了weak alias的⽅式替换。具体来说是
因为这些入口函数都被定义成了weak symbols,再加上gcc⽀持 alias attribute,所以替换就变
成了这种通用形式: void* malloc(size_t size) THROW attribute__ ((alias (tc_malloc))) 因此所有malloc的调用都跳转到了tc_malloc的实现。
具体参考这里 : GCC attribute 之weak,alias属性_gcc weak alias-CSDN博客
有些平台不⽀持这样的东西,需要使⽤hook的钩⼦技术来做: Hook技术 - zzfx - 博客园
我们这个项目主要是学习 tcmalloc 的精华部分并不完全照搬 , 所有有些东西没有完善 , 比如跨平台的TLS , 64位下的基数树改写等等 , 大家可以自行尝试。

