内存池代码分析
我们都知道内存池是一种分配内存并进行重新利用得技术,通过减少频繁得动态内存分配与释放操作从而提高程序运行效率。
这里代码分析的是 代码随想录 开源的内存池 memory-pool
CentralCache.h
CentralCache(中心缓存)是内存池架构中介于线程缓存(TreadCache)和页缓存(PageCahce)之间的中间层。
其核心功能是:
- 缓存小内存块 ——– 从页缓存申请大块内存,切割成固定大小的内存块供线程缓存获取
- 线程间共享 ——– 是全局共享,通过锁保证线程安全
- 批量分配并回收——– 减少ThreadCache访问频率,减低内存碎片和锁竞争,提高效率
#include "Common.h" //包含通用定义
#include //包含原子操作 、锁操作
核心定义CentralCache对象并在Kama_memoryPool命名空间里避免命名冲突。
static CentralCache& getInstance()
{
static CentralCache instance;
return instance;
}
使用单例模式:全局唯一的中心缓存实例,并使用局部静态变量保证线程安全且只初始化一次。
局部静态变量初始化对于C++11来说这是安全且高效的,不用手动加锁。
下面然我们进行分析核心对外接口
void* fetchRange(size_t index, size_t &batchNum);
void returnRange(void* start, size_t size, size_t bytes);
对于fetchRange函数来说:
- index对应内存块的下标,按照Common.h中的映射
- batch对应传入 “想要的内存块数量”,返回 “实际拿到的数量”(可能中心缓存不够,只能返回部分)
对于returnRange函数来说: - start是要归还的内存块链表头
- size即是内存块的数量
- Bytes是单个内存块的大小
作用是ThreadCache 中某个大小的内存块过多时,批量归还给 CentralCache,避免内存浪费。
让我们来看它的私有构造函数
private:
// 私有构造函数:禁止外部创建实例(单例的关键)
CentralCache()
{
// 初始化中心缓存的自由链表:所有原子指针置为nullptr
for (auto& ptr : centralFreeList_)
{
ptr.store(nullptr, std::memory_order_relaxed);
}
// 初始化所有自旋锁:清空(atomic_flag默认是clear状态,即未锁定)
for (auto& lock : locks_)
{
lock.clear();
}
}
- 构造函数私有化:确保只能通过 getInstance () 获取实例,杜绝 new CentralCache () 等外部创建方式,是单例模式实现的核心保障
- 自由链表初始化:遍历 centralFreeList_数组,将所有原子指针通过 store 置为 nullptr,memory_order_relaxed 内存序保证原子操作本身,无额外指令重排限制,初始化场景下性能最优
- 自旋锁初始化:遍历 locks_数组调用 clear (),将所有自旋锁置为未锁定状态,为后续多线程访问做准备
接下来分析私有内部方法
// 从页缓存(PageCache)获取大块内存,切割后补充到中心缓存
void* fetchFromPageCache(size_t size);
- 核心作用:作为 CentralCache 的兜底逻辑,当某个尺寸的内存块库存不足时,向页缓存申请大块内存(通常按物理页粒度申请),切割为对应大小的小内存块后补充到自由链表中
- 访问控制:私有化设计,外部组件(如 ThreadCache)无需直接调用,仅通过 fetchRange 间接触发,符合封装原则,避免内部逻辑暴露
核心成员变量
private:
// 中心缓存的自由链表数组:每个元素对应一种大小的内存块链表头(原子指针保证线程安全)
std::array, FREE_LIST_SIZE> centralFreeList_;
// 用于同步的自旋锁
std::array locks_;
对于 centralFreeList_来说:
- 容器选择:std::array 固定长度数组,长度由 FREE_LIST_SIZE 定义,相比动态容器访问效率更高,内存布局更稳定
- 原子指针:std::atomic<void*> 保证多线程对链表头的修改是原子操作,避免并发场景下链表指针错乱,保证数据一致性
- 映射关系:数组下标与内存块尺寸一一对应(如下标 0 对应 8B、下标 1 对应 16B),每个元素指向对应尺寸的空闲内存块链表头
对于 locks_来说:
- 锁类型:std::atomic_flag 轻量级自旋锁,相比 std::mutex 开销更小,适合内存分配这种短时间锁定的场景
- 细粒度锁设计:每个自由链表对应一把锁,而非全局一把锁,线程操作不同尺寸链表时互不阻塞,大幅提升多线程并发效率
PageCache.h
PageCache (页缓存) 是内存池架构中最底层的内存管理组件,直接对接操作系统进行内存申请与释放,是 CentralCache 的内存来源。
其核心功能是:
- 页级内存管理 ——– 以物理页(4KB)为单位申请 / 管理内存,避免频繁调用系统 API
- 内存碎片合并 ——– 回收内存页时合并相邻空闲页,减少内存碎片
- 全局统一调度 ——– 为 CentralCache 提供大块内存,按需切割后向下分发
#include "Common.h" //包含通用定义
#include
核心定义 PageCache 对象并在 Kama_memoryPool 命名空间里避免命名冲突。
static const size_t PAGE_SIZE = 4096;
static PageCache& getInstance(){
static PageCache instance;
return instance;
}
- 页大小定义:PAGE_SIZE 固定为 4096 字节(即 4KB),符合 x86/x64 系统的默认物理页大小,是操作系统内存管理的基本单位
- 单例模式:和 CentralCache 一致
下面让我们进行分析核心对外接口
void* allocateSpan(size_t numPages);
void deallocateSpan(void* ptr, size_t numPages);
对于 allocateSpan 函数来说:
- numPages 是要申请的物理页数量(比如 numPages=2 表示申请 8KB 内存)
- 返回值是申请到的连续内存页起始地址,供 CentralCache 切割为小内存块使用
对于 deallocateSpan 函数来说:
- ptr 是要回收的内存页起始地址
- numPages 是要回收的物理页数量
- 作用是将 CentralCache 归还的内存页回收,合并相邻空闲页后重新存入空闲页表,实现内存复用
让我们来看它的私有构造函数
private:
PageCache() = default;
void* systemAlloc(size_t numPages);
- 构造函数默认化:使用 = default 显式生成默认构造函数,同时私有化避免外部创建实例,是单例模式的必要保障
- systemAlloc 私有方法:作为底层系统调用封装,直接调用 mmap/brk 等系统 API 申请内存(不同平台实现不同),对外屏蔽系统级内存申请的细节,符合封装原则
接下来分析核心内部数据结构
private:
struct Span{
void* pageAddr; // 内存页起始地址
size_t numPages; // 该Span包含的物理页数量
Span* next; // 串联同尺寸的空闲Span链表
};
- Span 结构作用:作为页缓存的核心管理单元,描述一段连续的物理内存页
- 关键字段解读:
- pageAddr:标记该段内存的起始地址,是内存定位的核心标识
- numPages:记录包含的页数,用于计算内存大小和合并相邻 Span
- next:将相同页数的空闲 Span 串联成链表,方便快速查找和分配
最后分析核心成员变量
private:
std::map freeSpans_;
std::map spanMap_;
std::mutex mutex_;
对于 freeSpans_来说:
- 容器类型:std::map <页数,Span 链表头>,按页数作为 key 组织空闲 Span
- 核心作用:快速查找对应页数的空闲 Span,比如 CentralCache 需要 8 页内存时,直接通过 key=8 找到对应的空闲 Span 链表,分配效率高
- 有序特性:std::map 的有序性保证按页数从小到大 / 从大到小遍历,方便实现 “按需分配” 或 “合并分配” 逻辑
对于 spanMap_来说:
- 容器类型:std::map <内存地址,Span 指针>,按内存起始地址映射到对应的 Span
- 核心作用:回收内存时,通过内存地址快速定位到所属的 Span,比如知道某块内存的起始地址,就能立刻找到对应的 numPages 和相邻 Span,是内存合并的关键
对于 mutex_来说:
- 锁类型:std::mutex 全局互斥锁,相比 CentralCache 的细粒度自旋锁,页缓存使用粗粒度锁
- 设计原因:页缓存是内存池最底层,直接对接系统调用,操作频率低但单次操作耗时较长(系统调用是毫秒级),用互斥锁更适合,且全局锁实现简单,避免复杂的锁管理
MemoryPool.h
MemoryPool 是整个内存池架构的对外统一入口,封装了底层 ThreadCache/CentralCache/PageCache 的所有细节,给用户提供极简的内存分配 / 释放接口。
- 接口归一化 ——– 对外仅暴露 allocate/deallocate 两个静态方法,屏蔽内存池三层架构的复杂逻辑
- 调用转发 ——– 所有内存申请 / 释放请求,统一转发给 ThreadCache 处理,是用户和内存池底层的 “桥梁”
- 易用性提升 ——– 无需关注内存池内部组件,调用静态方法即可完成内存分配与回收
#include "ThreadCache.h" //依赖线程缓存,所有请求最终转发给ThreadCache处理
核心定义 MemoryPool 类并在 Kama_memoryPool 命名空间里避免命名冲突,作为内存池对外暴露的唯一类。
static void* allocate(size_t size)
{
return ThreadCache::getInstance()->allocate(size);
}
- 函数作用:内存池对外提供的内存分配接口,用户只需传入要申请的内存大小,即可获取对应内存地址
- 实现逻辑:无任何额外处理,直接调用 ThreadCache 单例的 allocate 方法,将分配请求转发给线程缓存
- 设计优势:静态方法无需创建 MemoryPool 实例,用户调用方式极简(如 MemoryPool::allocate (1024)),降低使用成本
static void deallocate(void* ptr, size_t size)
{
ThreadCache::getInstance()->deallocate(ptr, size);
}
- 函数作用:内存池对外提供的内存释放接口,用户传入要释放的内存地址和大小,完成内存回收
- 实现逻辑:同样直接转发给 ThreadCache 单例的 deallocate 方法,由线程缓存处理后续的回收逻辑(如判断是否归还给 CentralCache)
- 入参说明
- ptr:待释放的内存起始地址(必须是通过 MemoryPool::allocate 申请的地址)
- size:释放内存的大小(需和申请时的大小一致,保证回收时能正确匹配对应尺寸的自由链表)
ThreadCache.h
ThreadCache (线程缓存) 是内存池架构中最贴近用户层的组件,也是整个内存池高性能的核心保障。作为 CentralCache 的上层缓存,它为每个线程提供私有内存空间:
- 无锁访问 ——– 线程私有设计,分配 / 释放内存时无需加锁,极致提升并发效率
- 按需缓存 ——– 从 CentralCache 批量获取内存块缓存,减少跨线程 / 跨层调用
- 阈值回收 ——– 缓存的内存块数量超过阈值时,自动归还给 CentralCache,避免内存浪费
static ThreadCache* getInstance() {
static ThreadCache instance;
return &instance;
}
线程私有设计说明:这里的 “单例” 实际是线程局部单例(通常配合 TLS 线程局部存储实现,代码中未显式体现但逻辑上是核心设计),即每个线程拥有独立的 ThreadCache 实例,这是无锁访问的关键
下面让我们进行分析核心对外接口
void* allocate(size_t size);
void deallocate(void* ptr, size_t size);
对于 allocate 函数来说:
- size 是用户申请的内存大小(未对齐),函数内部会先通过 SizeClass::roundUp 对齐到 8 的倍数
- 核心逻辑:优先从当前线程的 freeList_中取内存块,若缓存不足则调用 fetchFromCentralCache 批量获取
- 返回值:对齐后内存块的可用地址(跳过 BlockHeader 元数据)
对于 deallocate 函数来说: - ptr 是用户要释放的内存地址,size 是申请时的原始大小(或对齐后的大小)
- 核心逻辑:将内存块归还给当前线程的 freeList_,若 freeListSize_超过阈值则调用 returnToCentralCache 批量归还
- 设计优势:线程内操作无锁,相比直接调用 malloc/free,性能提升数倍
让我们来看它的私有构造函数与内部方法
private:
ThreadCache() = default;
void* fetchFromCentralCache(size_t index);
void returnToCentralCache(void* ptr, size_t size);
size_t getBatchNum(size_t size);
bool shouldReturnToCentralCache(size_t size);
- 构造函数默认化:私有化保证只能通过 getInstance 获取实例,默认构造函数初始化 freeList_和 freeListSize_为空
- fetchFromCentralCache:
- index 是内存块尺寸对应的自由链表下标
- 作用:向 CentralCache 批量申请内存块,补充到当前线程的 freeList_中,是 ThreadCache 的 “补库存” 逻辑
- returnToCentralCache:
- ptr 是要归还的内存块链表头,size 是单块内存大小
- 作用:将线程缓存中过剩的内存块批量归还给 CentralCache,避免单个线程占用过多内存
- getBatchNum:
- 作用:根据内存块大小计算 “批量申请 / 归还的数量”(小内存块批量数多,大内存块批量数少)
- 设计逻辑:平衡 “减少 CentralCache 访问次数” 和 “避免内存浪费”,比如 8B 内存块批量取 20 个,256KB 内存块批量取 2 个
- shouldReturnToCentralCache:
- 作用:判断当前尺寸的内存块缓存数量是否超过阈值,返回 bool 值决定是否需要归还
- 阈值规则:通常内存块越大,阈值越低(比如 8B 块缓存 50 个才归还,256KB 块缓存 5 个就归还)
分析核心成员变量
private:
std::array freeList_;
std::array freeListSize_;
对于 freeList_来说:
- 容器类型:std::array<void*, FREE_LIST_SIZE>,长度和 Common.h 中定义的尺寸映射一致
- 核心作用:存储当前线程的空闲内存块链表头,每个下标对应一种对齐后的内存尺寸(如下标 0→8B、下标 1→16B)
- 设计优势:数组访问 O (1) 时间复杂度,线程内取内存块无需遍历,极致高效
- 对于 freeListSize_来说:
- 容器类型:std::array<size_t, FREE_LIST_SIZE>,和 freeList_一一对应
- 核心作用:记录每个尺寸的空闲内存块数量,是判断 “是否需要补库存”“是否需要归还” 的核心依据
- 举例:freeListSize_[0]=10 表示当前线程缓存了 10 个 8B 的空闲内存块,若用户申请 8B 内存,直接取链表头并将数量减 1 即可
Common.h
Common.h 是整个内存池架构的基础规则定义文件,封装了内存池的核心常量、内存块元数据结构和尺寸计算工具,是 ThreadCache/CentralCache/PageCache 所有组件的公共依赖,保证整个内存池遵循统一的尺寸规则。
其核心作用是:
- 规则标准化 ——– 定义内存对齐粒度、最大管理尺寸等核心常量,统一所有组件的尺寸逻辑
- 元数据封装 ——– 定义内存块头部结构,管理内存块的状态和链表关系
- 工具类封装 ——– 提供内存对齐、尺寸转下标等核心算法,避免重复代码
#include //提供size_t等基础类型,是内存大小/下标表示的核心依赖
#include //为后续原子操作预留头文件(如CentralCache的原子指针)
#include //为自由链表数组提供容器支持
constexpr size_t ALIGNMENT = 8;
constexpr size_t MAX_BYTES = 256 * 1024; //256kb
constexpr size_t FREE_LIST_SIZE = MAX_BYTES / ALIGNMENT;
- 取值定义:固定为 8 字节,是 x86/x64 系统的最优内存对齐粒度
- 设计原因:8 字节是 C++ 基本类型(如指针、int64、double)的自然对齐要求,同时能保证 CPU 访问内存时不跨缓存行,最大化访问效率
- 核心作用:所有内存池分配的小内存块,大小必须是 8 的整数倍,保证尺寸标准化
- 对于 MAX_BYTES 来说:
- 取值定义:256KB,是内存池管理的最大单块内存尺寸
- 设计原因:内存池的优势是管理 “小内存”(减少碎片、提升分配速度),超过 256KB 的内存请求直接调用系统 malloc,避免内存池切割大块内存导致的浪费
- 边界意义:划分 “内存池管理范围” 和 “系统调用范围” 的核心阈值
对于 FREE_LIST_SIZE 来说: - 计算逻辑:256*1024 / 8 = 32768,是自由链表数组的固定长度
- 核心作用:对应 “8B~256KB、步长 8B” 的所有内存块尺寸,每个下标对应一种尺寸,是 ThreadCache/CentralCache 自由链表数组的长度依据
- 常量特性:用 constexpr 定义为编译期常量,相比宏定义类型更安全,且无需运行时计算,性能更优
struct BlockHeader {
size_t size;
bool inUse;
BlockHeader* next;
};
- 结构作用:作为每个内存块的 “头部元数据”,嵌入在实际内存块的起始位置,是内存池管理内存块的核心
- 关键字段解读:
- size:记录该内存块的实际大小(已对齐后的尺寸),回收时能快速定位到对应自由链表下标
- inUse:标记内存块是否被使用(true = 使用中,false = 空闲),避免重复分配 / 重复回收的错误
- next:链表指针,将同尺寸的空闲内存块串联成链表,是自由链表的核心组成部分
- 内存布局说明:用户实际拿到的内存地址是 BlockHeader 之后的区域,回收时通过指针偏移找回头部
class SizeClass {
public:
static size_t roundUp(size_t bytes) {
return (bytes + ALIGNMENT - 1) & ~(ALIGNMENT - 1);
}
static size_t getIndex(size_t bytes) {
bytes = std::max(bytes, ALIGNMENT);
return (bytes + ALIGNMENT - 1) / ALIGNMENT - 1;
}
};
- 核心作用:将用户输入的任意字节数向上取整到最近的 8 的倍数(实现内存对齐)
- 算法亮点:用位运算替代除法(~(ALIGNMENT-1) 即~7,二进制为…11111000),按位与会清空最后 3 位,计算效率比除法高 10 倍以上
- 示例:输入 10→10+7=17→17&
7=16;输入 8→8+7=15→15&7=8(刚好对齐时不变)
对于 getIndex 函数来说: - 核心作用:将对齐后的内存尺寸转换为自由链表数组的下标
- 边界处理:std::max (bytes, ALIGNMENT) 确保最小尺寸为 8B,避免用户传入 0B/1B 等非法尺寸导致下标为负
- 算法逻辑:(bytes +7)/8 -1 等价于 “向上取整到 8 的倍数后 ÷8 -1”,示例:
- 输入 8→(8+7)/8=1→1-1=0(下标 0,对应 8B 链表)
- 输入 10→先 roundUp 到 16→(16+7)/8=2→2-1=1(下标 1,对应 16B 链表)
- 设计优势:所有组件复用同一套下标映射规则,修改对齐粒度仅需调整 ALIGNMENT 常量
ThreadCache.cpp
ThreadCache.cpp 是 ThreadCache 类的核心实现文件,完整落地了线程缓存的内存分配、释放、批量申请 / 归还逻辑,是整个内存池 “无锁高性能” 特性的核心载体。相比头文件的接口定义,该文件聚焦具体的逻辑实现,体现了线程缓存 “优先用本地缓存、批量和中心缓存交互” 的核心设计。
#include "../include/ThreadCache.h" //引入线程缓存头文件,关联类定义
#include "../include/CentralCache.h" //引入中心缓存头文件,实现跨层交互
#include //引入malloc/free,处理超大内存请求
所有实现逻辑均在 Kama_memoryPool 命名空间下,保证代码隔离性。
核心分配逻辑:allocate 方法
void* ThreadCache::allocate(size_t size)
{
if (size == 0)
{
size = ALIGNMENT;
}
if (size > MAX_BYTES)
{
return malloc(size);
}
size_t index = SizeClass::getIndex(size);
freeListSize_[index]--;
if (void* ptr = freeList_[index])
{
freeList_[index] = *reinterpret_cast(ptr);
return ptr;
}
return fetchFromCentralCache(index);
}
- 边界处理:
- 若用户传入 0,默认分配 8 字节(最小对齐单位),避免非法尺寸;
- 若尺寸超过 256KB,直接调用系统 malloc,符合内存池 “只管理小内存” 的规则;
- 核心流程:
计算尺寸对应的自由链表下标 → 2. 缓存数量减 1 → 3. 若本地缓存有空闲块,直接取链表头并更新链表 → 4. 缓存不足则向 CentralCache 批量申请; - 链表操作细节:*reinterpret_cast<void**>(ptr) 是内存池经典的 “指针内嵌链表” 技巧 —— 将内存块的首地址作为指针,存储下一个空闲块的地址,无需额外的链表节点,节省内存。
void ThreadCache::deallocate(void* ptr, size_t size)
{
if (size > MAX_BYTES)
{
free(ptr);
return;
}
size_t index = SizeClass::getIndex(size);
*reinterpret_cast(ptr) = freeList_[index];
freeList_[index] = ptr;
freeListSize_[index]++;
if (shouldReturnToCentralCache(index))
{
returnToCentralCache(freeList_[index], size);
}
}
- 边界处理:超大内存直接调用系统 free,和 allocate 逻辑一致;
核心流程:
计算下标 → 2. 将释放的内存块插入本地自由链表头部 → 3. 缓存数量加 1 → 4. 若超过阈值则批量归还给 CentralCache; - 插入链表技巧:先将当前内存块的首地址指向原链表头,再更新链表头为当前内存块,O (1) 时间完成插入,效率拉满
阈值判断:shouldReturnToCentralCache 方法
bool ThreadCache::shouldReturnToCentralCache(size_t index)
{
size_t threshold = 64;
return (freeListSize_[index] > threshold);
}
- 核心逻辑:简单阈值判断 —— 只要某尺寸的缓存数量超过 64,就触发归还逻辑;
- 设计说明:示例中用固定阈值 64,实际生产环境中通常会按内存块大小动态调整(比如小内存阈值高、大内存阈值低),避免大内存块占用过多线程缓存。
批量申请:fetchFromCentralCache 方法
void* ThreadCache::fetchFromCentralCache(size_t index)
{
size_t size = (index + 1) * ALIGNMENT;
size_t batchNum = getBatchNum(size);
void* start = CentralCache::getInstance().fetchRange(index, batchNum);
if (!start) return nullptr;
freeListSize_[index] += batchNum-1;
void* result = start;
if (batchNum > 1)
{
freeList_[index] = *reinterpret_cast(start);
}
return result;
}
核心流程:
计算当前下标对应的内存块大小 → 2. 计算批量申请数量 → 3. 调用 CentralCache 获取批量内存块 → 4. 留 1 个给当前请求,剩余的存入本地自由链表;
数量处理细节:freeListSize_[index] += batchNum-1—— 因为 1 个内存块直接返回给用户,剩余的 batchNum-1 个存入缓存,数量统计精准。
批量归还:returnToCentralCache 方法
void ThreadCache::returnToCentralCache(void* start, size_t size)
{
size_t index = SizeClass::getIndex(size);
size_t alignedSize = SizeClass::roundUp(size);
size_t batchNum = freeListSize_[index];
if (batchNum <= 1) return;
size_t keepNum = std::max(batchNum / 4, size_t(1));
size_t returnNum = batchNum - keepNum;
char* current = static_cast(start);
char* splitNode = current;
for (size_t i = 0; i < keepNum - 1; ++i)
{
splitNode = reinterpret_cast(*reinterpret_cast(splitNode));
if (splitNode == nullptr)
{
returnNum = batchNum - (i + 1);
break;
}
}
if (splitNode != nullptr)
{
void* nextNode = *reinterpret_cast(splitNode);
*reinterpret_cast(splitNode) = nullptr;
freeList_[index] = start;
freeListSize_[index] = keepNum;
if (returnNum > 0 && nextNode != nullptr)
{
CentralCache::getInstance().returnRange(nextNode, returnNum * alignedSize, index);
}
}
}
- 核心逻辑:“留一部分、还一部分”—— 保留 1/4 的缓存(最少 1 个),剩余的归还给 CentralCache,既避免线程缓存空了频繁申请,又避免缓存过多浪费内存;
- 链表拆分细节:
遍历链表找到第 keepNum 个节点 → 2. 切断链表(将该节点的 next 置空) → 3. 更新本地缓存为前 keepNum 个节点 → 4. 将剩余节点归还给 CentralCache; - 容错处理:遍历中若链表提前结束,动态调整归还数量,避免空指针错误。
批量数计算:getBatchNum 方法
size_t ThreadCache::getBatchNum(size_t size)
{
constexpr size_t MAX_BATCH_SIZE = 4 * 1024; // 4KB
size_t baseNum;
if (size <= 32) baseNum = 64; // 64 * 32 = 2KB
else if (size <= 64) baseNum = 32; // 32 * 64 = 2KB
else if (size <= 128) baseNum = 16; // 16 * 128 = 2KB
else if (size <= 256) baseNum = 8; // 8 * 256 = 2KB
else if (size <= 512) baseNum = 4; // 4 * 512 = 2KB
else if (size <= 1024) baseNum = 2; // 2 * 1024 = 2KB
else baseNum = 1;
size_t maxNum = std::max(size_t(1), MAX_BATCH_SIZE / size);
return std::max(sizeof(1), std::min(maxNum, baseNum));
}
- 核心设计:“固定总字节数” 原则 —— 小内存块批量数多,大内存块批量数少,保证每次批量申请的总字节数约 2KB(不超过 4KB 上限);
比如 32B 内存块批 64 个(6432=2048B),1024B 内存块批 2 个(21024=2048B); - 边界处理:
- MAX_BATCH_SIZE / size 避免批量数过大导致单次申请内存过多;
- std::max(sizeof(1), …) 保证最少申请 1 个,避免 0 个的非法情况。
PageCache.cpp
PageCache.cpp 是页缓存的核心实现文件,落地了以物理页为单位的内存申请、回收、合并逻辑,是内存池对接操作系统的 “最后一公里”。相比上层的 ThreadCache/CentralCache,该文件聚焦 “页级内存管理” 和 “内存碎片合并”,体现了内存池 “底层统一调度、减少系统调用” 的核心设计.
#include "PageCache.h" //引入页缓存头文件,关联类定义
#include //引入mmap系统调用,实现跨平台内存申请(Linux/macOS)
#include //引入memset,初始化申请的内存空间
核心分配逻辑:allocateSpan 方法
void* PageCache::allocateSpan(size_t numPages)
{
std::lock_guard lock(mutex_);
auto it = freeSpans_.lower_bound(numPages);
if (it != freeSpans_.end())
{
Span* span = it->second;
if (span->next)
{
freeSpans_[it->first] = span->next;
}
else
{
freeSpans_.erase(it);
}
if (span->numPages > numPages)
{
Span* newSpan = new Span;
newSpan->pageAddr = static_cast(span->pageAddr) +
numPages * PAGE_SIZE;
newSpan->numPages = span->numPages - numPages;
newSpan->next = nullptr;
auto& list = freeSpans_[newSpan->numPages];
newSpan->next = list;
list = newSpan;
span->numPages = numPages;
}
spanMap_[span->pageAddr] = span;
return span->pageAddr;
}
void* memory = systemAlloc(numPages);
if (!memory) return nullptr;
Span* span = new Span;
span->pageAddr = memory;
span->numPages = numPages;
span->next = nullptr;
spanMap_[memory] = span;
return memory;
}
- 线程安全保障:std::lock_guardstd::mutex 自动加锁 / 解锁,保证页缓存全局操作的线程安全;
- 核心流程(优先复用空闲页):
用lower_bound查找 “大于等于目标页数” 的空闲 Span(有序 map 的高效查找);
若找到空闲 Span:
从 freeSpans_中移除该 Span;
若 Span 页数超过目标页数,拆分出剩余页数为新 Span,重新存入 freeSpans_;
将分配的 Span 存入 spanMap_,返回内存地址; - 若未找到空闲 Span:调用 systemAlloc 向操作系统申请新内存,封装为 Span 后返回;
核心回收逻辑:deallocateSpan 方法
void PageCache::deallocateSpan(void* ptr, size_t numPages)
{
std::lock_guard lock(mutex_);
auto it = spanMap_.find(ptr);
if (it == spanMap_.end()) return;
Span* span = it->second;
void* nextAddr = static_cast(ptr) + numPages * PAGE_SIZE;
auto nextIt = spanMap_.find(nextAddr);
if (nextIt != spanMap_.end())
{
Span* nextSpan = nextIt->second;
bool found = false;
auto& nextList = freeSpans_[nextSpan->numPages];
if (nextList == nextSpan)
{
nextList = nextSpan->next;
found = true;
}
else if (nextList)
{
Span* prev = nextList;
while (prev->next)
{
if (prev->next == nextSpan)
{
prev->next = nextSpan->next;
found = true;
break;
}
prev = prev->next;
}
}
if (found)
{
span->numPages += nextSpan->numPages;
spanMap_.erase(nextAddr);
delete nextSpan;
}
}
auto& list = freeSpans_[span->numPages];
span->next = list;
list = span;
}
- 核心目标:回收内存页并合并相邻空闲页,减少内存碎片;
核心流程: - 从 spanMap_找到待回收的 Span(通过内存地址快速定位);
- 检查当前 Span 的下一个地址是否存在空闲 Span(相邻页合并的关键);
若存在相邻空闲 Span: - 从 freeSpans_中移除该相邻 Span;
- 合并两个 Span 的页数,释放相邻 Span 的内存;
- 将合并后的 Span 重新存入 freeSpans_,等待后续分配;
合并逻辑细节:遍历 freeSpans_中的 Span 链表,找到相邻 Span 并移除,保证合并后链表的完整性。
void* PageCache::systemAlloc(size_t numPages)
{
size_t size = numPages * PAGE_SIZE;
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) return nullptr;
memset(ptr, 0, size);
return ptr;
}
- 核心实现:调用 Linux/macOS 系统的mmap函数申请内存(Windows 对应 VirtualAlloc);
- 参数解读:
- nullptr:让系统自动选择内存起始地址;
- PROT_READ | PROT_WRITE:内存可读写;
- MAP_PRIVATE | MAP_ANONYMOUS:私有匿名内存(不关联文件,纯内存分配);
- -1:匿名内存无需文件描述符;
- 容错处理:检查mmap返回值(MAP_FAILED 表示失败),返回 nullptr 避免非法地址;
- 内存初始化:memset(ptr, 0, size) 将申请的内存置 0,避免脏数据问题。
ThreadCache.cpp
ThreadCache.cpp 是 ThreadCache 类的核心实现文件,完整落地了线程缓存的内存分配、释放、批量申请 / 归还逻辑,是整个内存池 “无锁高性能” 特性的核心载体。相比头文件的接口定义,该文件聚焦具体的逻辑实现,体现了线程缓存 “优先用本地缓存、批量和中心缓存交互” 的核心设计。
#include "../include/ThreadCache.h" //引入线程缓存头文件,关联类定义
#include "../include/CentralCache.h" //引入中心缓存头文件,实现跨层交互
#include //引入malloc/free,处理超大内存请求
核心分配逻辑:allocate 方法
void* ThreadCache::allocate(size_t size)
{
if (size == 0)
{
size = ALIGNMENT;
}
if (size > MAX_BYTES)
{
return malloc(size);
}
size_t index = SizeClass::getIndex(size);
freeListSize_[index]--;
if (void* ptr = freeList_[index])
{
freeList_[index] = *reinterpret_cast(ptr);
return ptr;
}
return fetchFromCentralCache(index);
}
- 边界容错处理:
- 针对用户传入 0 字节的非法请求,默认分配最小对齐单位(8 字节),避免后续下标计算、链表操作出现异常;
- 超过内存池管理上限(256KB)的内存请求,直接转发给系统malloc,遵循 “内存池只优化小内存分配” 的设计原则;
- 核心分配流程(无锁高效):
- 调用SizeClass::getIndex将申请尺寸转换为自由链表下标,完成尺寸标准化;
- 先将对应下标的缓存数量减 1(预判分配,简化逻辑);
- 若本地自由链表有空闲块,通过指针内嵌链表技巧取出头节点:将当前链表头更新为该块指向的下一个空闲块,直接返回当前块;
- 本地缓存不足时,调用fetchFromCentralCache向中心缓存批量申请补充;
- 关键技巧解析:*reinterpret_cast<void**>(ptr) 是内存池经典优化 —— 直接复用内存块的首地址存储下一个空闲块的指针,无需额外定义链表节点结构体,几乎无元数据开销。
核心释放逻辑:deallocate 方法
void ThreadCache::deallocate(void* ptr, size_t size)
{
if (size > MAX_BYTES)
{
free(ptr);
return;
}
size_t index = SizeClass::getIndex(size);
*reinterpret_cast(ptr) = freeList_[index];
freeList_[index] = ptr;
freeListSize_[index]++;
if (shouldReturnToCentralCache(index))
{
returnToCentralCache(freeList_[index], size);
}
}
- 对称化设计:超大内存释放直接调用系统free,与allocate逻辑完全匹配,保证上层接口的一致性;
- 核心释放流程(O (1) 时间复杂度):
- 计算释放尺寸对应的链表下标;
- 头插法将释放块加入本地自由链表:先让该块指向原链表头,再将链表头更新为该块;
- 缓存数量加 1,判断是否超过阈值;
- 超过阈值则触发批量归还逻辑,避免单个线程占用过多内存;
- 性能优势:全程无锁、无遍历,释放操作仅需 3 次指针赋值 + 1 次计数,耗时可忽略。
阈值判断:shouldReturnToCentralCache 方法
bool ThreadCache::shouldReturnToCentralCache(size_t index)
{
size_t threshold = 64;
return (freeListSize_[index] > threshold);
}
- 核心逻辑:采用固定阈值 64 作为归还触发条件,当某尺寸的空闲块数量超过 64 时,向中心缓存归还部分块;
- 设计说明:示例中固定阈值是为了简化逻辑,生产级实现中通常会按块大小动态调整(如 8B 块阈值设为 128,256KB 块阈值设为 8),平衡 “减少中心缓存交互” 和 “内存全局复用”。
批量申请:fetchFromCentralCache 方法
void* ThreadCache::fetchFromCentralCache(size_t index)
{
size_t size = (index + 1) * ALIGNMENT;
size_t batchNum = getBatchNum(size);
void* start = CentralCache::getInstance().fetchRange(index, batchNum);
if (!start) return nullptr;
freeListSize_[index] += batchNum-1;
void* result = start;
if (batchNum > 1)
{
freeList_[index] = *reinterpret_cast(start);
}
return result;
}
- 批量策略核心:
- 由下标反算内存块实际大小(下标 + 1)*8;
- 调用getBatchNum计算合理的批量数(小块多批、大块少批);
- 向中心缓存申请批量块,补充本地缓存;
- 缓存处理细节:
- 申请到的批量块中,1 个直接返回给当前请求,剩余batchNum-1个存入本地链表;
- freeListSize_仅累加剩余数量,保证计数精准,避免重复统计。
批量归还:returnToCentralCache 方法
void ThreadCache::returnToCentralCache(void* start, size_t size)
{
size_t index = SizeClass::getIndex(size);
size_t alignedSize = SizeClass::roundUp(size);
size_t batchNum = freeListSize_[index];
if (batchNum <= 1) return;
size_t keepNum = std::max(batchNum / 4, size_t(1));
size_t returnNum = batchNum - keepNum;
char* current = static_cast(start);
char* splitNode = current;
for (size_t i = 0; i < keepNum - 1; ++i)
{
splitNode = reinterpret_cast(*reinterpret_cast(splitNode));
if (splitNode == nullptr)
{
returnNum = batchNum - (i + 1);
break;
}
}
if (splitNode != nullptr)
{
void* nextNode = *reinterpret_cast(splitNode);
*reinterpret_cast(splitNode) = nullptr;
freeList_[index] = start;
freeListSize_[index] = keepNum;
if (returnNum > 0 && nextNode != nullptr)
{
CentralCache::getInstance().returnRange(nextNode, returnNum * alignedSize, index);
}
}
}
- 归还策略:“保留 1/4、归还 3/4”,既避免本地缓存被清空导致频繁申请,又防止线程独占过多内存;
- 链表拆分逻辑:
- 遍历链表找到第keepNum个节点,作为本地缓存和归还块的分割点;
- 切断链表(置空分割点的 next 指针),更新本地链表为前keepNum个节点;
- 将剩余节点批量归还给中心缓存;
- 容错处理:遍历中若链表提前结束,动态调整归还数量,避免空指针访问,保证代码健壮性。
批量数计算:getBatchNum 方法
size_t ThreadCache::getBatchNum(size_t size)
{
constexpr size_t MAX_BATCH_SIZE = 4 * 1024; // 4KB
size_t baseNum;
if (size <= 32) baseNum = 64; // 64 * 32 = 2KB
else if (size <= 64) baseNum = 32; // 32 * 64 = 2KB
else if (size <= 128) baseNum = 16; // 16 * 128 = 2KB
else if (size <= 256) baseNum = 8; // 8 * 256 = 2KB
else if (size <= 512) baseNum = 4; // 4 * 512 = 2KB
else if (size <= 1024) baseNum = 2; // 2 * 1024 = 2KB
else baseNum = 1;
size_t maxNum = std::max(size_t(1), MAX_BATCH_SIZE / size);
return std::max(sizeof(1), std::min(maxNum, baseNum));
}
- 核心设计原则:“固定总字节数”—— 无论块大小,单次批量申请的总字节数控制在 2KB 左右(不超过 4KB 上限),避免单次申请过多内存;
- 示例:32B 块批 64 个(总计 2KB)、1024B 块批 2 个(总计 2KB),兼顾申请效率和内存占用;
- 边界防护:
- MAX_BATCH_SIZE / size 避免大内存块批量数过大(如 256KB 块仅批 1 个);
- std::max(sizeof(1), …) 保证最少申请 1 个,杜绝 0 个的非法情况;
- 小瑕疵说明:std::max(sizeof(1), …) 中sizeof(1)是笔误(应为size_t(1)),但sizeof(1)值为 1,不影响逻辑结果。