内存池代码分析


内存池代码分析

我们都知道内存池是一种分配内存并进行重新利用得技术,通过减少频繁得动态内存分配与释放操作从而提高程序运行效率。
这里代码分析的是 代码随想录 开源的内存池 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       //用于存储空闲页块和地址映射
#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,不影响逻辑结果。

文章作者: D.riven
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 D.riven !
  目录