实现简化的高性能并发内存池

目录

引言:为什么需要内存池?

[标准库 malloc 的问题](#标准库 malloc 的问题)

解决方案:内存池

一、整体架构:三层缓存设计

设计哲学

二、核心数据结构

[2.1 FreeList:自由链表](#2.1 FreeList:自由链表)

[2.2 Span:内存跨度管理者](#2.2 Span:内存跨度管理者)

[2.3 SizeClass:内存对齐规则](#2.3 SizeClass:内存对齐规则)

三、第一层:ThreadCache(线程缓存)

[3.1 TLS 线程局部存储](#3.1 TLS 线程局部存储)

[3.2 分配流程:Allocate](#3.2 分配流程:Allocate)

[3.3 慢启动反馈调节算法](#3.3 慢启动反馈调节算法)

[3.4 释放流程:Deallocate](#3.4 释放流程:Deallocate)

四、第二层:CentralCache(中心缓存)

[4.1 桶锁机制](#4.1 桶锁机制)

[4.2 获取 Span:GetOneSpan](#4.2 获取 Span:GetOneSpan)

[4.3 批量获取:FetchRangeObj](#4.3 批量获取:FetchRangeObj)

五、第三层:PageCache(页缓存)

[5.1 核心职责](#5.1 核心职责)

[5.2 申请 Span:NewSpan](#5.2 申请 Span:NewSpan)

[5.3 Span 合并:ReleaseSpanToPagecache](#5.3 Span 合并:ReleaseSpanToPagecache)

六、关键优化:基数树

[6.1 为什么弃用 unordered_map](#6.1 为什么弃用 unordered_map)

[6.2 基数树(Radix Tree)原理](#6.2 基数树(Radix Tree)原理)

七、完整数据流图

[7.1 分配流程](#7.1 分配流程)

[7.2 释放流程](#7.2 释放流程)

八、性能测试

测试环境

测试结果

性能提升来源

九、核心设计思想总结

[9.1 分层解耦](#9.1 分层解耦)

[9.2 缓存友好](#9.2 缓存友好)

[9.3 反馈调节](#9.3 反馈调节)

[9.4 空间换时间](#9.4 空间换时间)

十、未来改进方向


引言:为什么需要内存池?

在高性能服务、后端开发以及基础组件设计中,内存分配效率直接决定程序整体吞吐与延迟。原生 malloc/free 在多线程场景下的短板愈发明显,而内存池正是解决这类问题的经典方案。

标准库 malloc 的问题

在多线程环境下直接使用系统 malloc/free,会面临三个核心痛点:

  1. 锁竞争严重:所有线程共享同一个堆空间,分配与释放必须加锁,高并发下大量线程阻塞,性能急剧下降。
  2. 小对象分配效率低:每次申请都触发系统调用,用户态与内核态切换开销巨大,小对象频繁申请尤其浪费资源。
  3. 内存碎片:大量小块内存随机分配与释放,会产生大量无法复用的外碎片与内碎片,导致内存利用率低下。

解决方案:内存池

内存池的核心思想是预分配 + 重复利用,从根本上减少系统调用与锁竞争:

复制代码
不用内存池:
  malloc() → 系统调用 → 加锁 → 返回
  free()   → 系统调用 → 加锁 → 返回

用内存池:
  malloc() → 从池里拿 → 几乎无锁 → 返回
  free()   → 放回池里 → 几乎无锁 → 返回

通过提前向系统申请大块内存,内部切分管理,让绝大多数分配、释放都在用户态完成,从而实现高性能并发内存管理。


一、整体架构:三层缓存设计

本项目参考 Google TCMalloc 设计思想,实现一套三层缓存架构的并发内存池,职责清晰、层次分明,兼顾速度与内存利用率。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    ThreadCache(线程缓存)                    │
│  • 每个线程独立拥有,通过 TLS 实现                          │
│  • 申请:桶里有直接拿,无锁!                               │
│  • 释放:桶里有位置放,无锁!                               │
│  • 桶满了/空了才需要找下一层                                │
└───────────────────────────┬─────────────────────────────────┘
                            │ 批量申请 / 批量释放
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    CentralCache(中心缓存)                   │
│  • 所有线程共享,单例模式                                   │
│  • 桶锁机制:每个桶独立加锁                                 │
│  • 管理多个 Span,每个 Span 挂载多个小对象                  │
│  • 负责从 PageCache 批量获取 Span                          │
└───────────────────────────┬─────────────────────────────────┘
                            │ 整 Span 归还
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    PageCache(页缓存)                       │
│  • 管理以页(8KB)为单位的内存                              │
│  • 负责向系统申请/释放大块内存                              │
│  • Span 合并:减少外碎片                                    │
│  • 核心结构:基数树(Radix Tree)快速查找页号→Span         │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                         系统堆                               │
│  • SystemAlloc / SystemFree                                 │
│  • 只有 PageCache 在大块内存不够时才会调用                   │
└─────────────────────────────────────────────────────────────┘

设计哲学

层级 角色 目标
ThreadCache 快速路径 99% 的分配 / 释放在这一层完成,完全无锁
CentralCache 协调者 减少锁竞争,批量调度内存对象
PageCache 管理者 管理大块内存,合并碎片,与系统交互

二、核心数据结构

内存池的高效依赖简洁、紧凑的数据结构,以下是三大核心结构设计。

2.1 FreeList:自由链表

用于管理切分好的小对象,采用先进后出的栈式管理,保证缓存友好与 O (1) 操作。

cpp 复制代码
class FreeList {
public:
    void push(void* obj);      // 头插 O(1)
    void* pop();               // 头删 O(1)
    void PushRange(...);       // 批量插入
    void PopRange(...);        // 批量删除

    bool Empty() { return _size == 0; }
    size_t Size() { return _size; }
    size_t& MaxSize() { return _maxSize; }  // 慢启动上限

private:
    void* _freeList = nullptr;  // 自由链表
    size_t _size = 0;           // 链表长度
    size_t _maxSize = 1;        // 批量申请上限
};

为什么用栈而非队列?

  • 后进先出,刚释放的对象大概率仍在 CPU 缓存中,缓存命中率更高;
  • 实现极简,仅需修改链表头指针即可完成入队、出队。

2.2 Span:内存跨度管理者

Span 是连续内存页的管理单元,所有小对象最终都挂载在 Span 上。

cpp 复制代码
struct Span {
    PAGE_ID _pageId;      // 起始页号(一页 = 8KB)
    size_t _n;            // 页数

    Span* _next = nullptr;
    Span* _prev = nullptr;

    size_t objectsize;    // 切分的小对象大小
    size_t _useCount;     // 当前正在使用的小对象数量

    void* _freeList;      // 小对象自由链表

    bool _isuse = false;  // 是否正在使用
};

**示例:**一个 Span 包含 3 页(24KB),切分成 16 byte 小对象:

cpp 复制代码
页 100        页 101        页 102
┌──────────┬──────────┬──────────┐
│ [16B][16B│][16B][16B│][16B][16B│]...
│  ↓        │         │          │
│  _freeList → [obj] → [obj] → ... → nullptr
└──────────┴──────────┴───────────┘
           ↑
     _pageId = 100
     _n = 3
     objectsize = 16
     _useCount = 0(全部空闲)

2.3 SizeClass:内存对齐规则

为统一管理不同大小对象,采用分级对齐策略,控制内碎片并简化索引计算。

大小范围 对齐数 说明
1 ~ 128 byte 8 byte 最小粒度
129 ~ 1024 byte 16 byte 稍大对象
1025 ~ 8192 byte 128 byte 中等对象
8193 ~ 65536 byte 1024 byte 较大对象
65537 ~ 262144 byte 8192 byte 大对象(上限 256KB)

对齐的意义:

  • 地址计算简单,可直接通过除法定位桶索引;
  • 严格控制内碎片,碎片率低于 10%;
  • 全局共 208 个桶,覆盖所有常见小对象场景。

三、第一层:ThreadCache(线程缓存)

ThreadCache 是整个内存池的快速路径,完全无锁,支撑绝大多数分配与释放。

3.1 TLS 线程局部存储

每个线程独享一个 ThreadCache,通过 TLS(线程局部存储)实现隔离:

cpp 复制代码
#if defined(__GNUC__) || defined(__clang__)
    #define TLS_DECLSPEC __thread
#elif defined(_MSC_VER)
    #define TLS_DECLSPEC __declspec(thread)
#endif

static TLS_DECLSPEC ThreadCache* pTLSThreadCache = nullptr;

**无锁原理:**线程之间互不访问对方缓存,天然无竞争,无需加锁。

复制代码
线程 A 的 ThreadCache          线程 B 的 ThreadCache
┌─────────────────┐            ┌─────────────────┐
│ 桶[0]: [obj]    │            │ 桶[0]: []       │
│ 桶[1]: []       │            │ 桶[1]: [obj]    │
│ 桶[2]: [obj]    │            │ 桶[2]: []       │
└─────────────────┘            └─────────────────┘
        ↑                               ↑
    只有 A 能访问                    只有 B 能访问

3.2 分配流程:Allocate

cpp 复制代码
void* ThreadCache::Allocate(size_t size) {
    // 1. 对齐计算
    size_t alignSize = SizeClass::RoundUp(size);
    size_t index = SizeClass::Index(size);

    // 2. 桶里有,直接弹出来
    if (!_freeLists[index].Empty()) {
        return _freeLists[index].pop();
    }

    // 3. 桶空了,找 CentralCache 批量申请
    return FetchFromCentralCache(index, alignSize);
}

3.3 慢启动反馈调节算法

ThreadCache 向中心缓存申请对象时,使用慢启动动态调整批量大小:

cpp 复制代码
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size) {
    // 1. 计算批量申请数量
    size_t batchNum = std::min(
        _freeLists[index].MaxSize(),
        SizeClass::NumMoveSize(size)
    );

    // 2. 达到上限则扩容,慢启动增长
    if (batchNum == _freeLists[index].MaxSize()) {
        _freeLists[index].MaxSize()++;
    }

    // 3. 批量获取
    void* start = nullptr;
    void* end = nullptr;
    size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(
        start, end, batchNum, size
    );

    // 4. 返回一个,其余入桶
    if (actualNum == 1)
        return start;
    else {
        _freeLists[index].PushRange(NextObj(start), end, actualNum - 1);
        return start;
    }
}

慢启动价值:

  • 避免一次性申请过多导致内存闲置;
  • 避免频繁申请导致频繁加锁、性能下降;
  • 上限随使用量逐步增长,自适应业务场景。

3.4 释放流程:Deallocate

cpp 复制代码
void ThreadCache::Deallocate(void* ptr, size_t size) {
    // 1. 找到对应桶插入
    size_t index = SizeClass::Index(size);
    _freeLists[index].push(ptr);

    // 2. 桶过长则批量归还中心缓存
    if (_freeLists[index].Size() >= _freeLists[index].MaxSize()) {
        ListTooLong(_freeLists[index], size);
    }
}

批量归还可大幅减少锁竞争,一次加锁完成大量对象回收。


四、第二层:CentralCache(中心缓存)

CentralCache 作为线程共享的中间层,负责批量调度内存,使用桶锁降低竞争。

4.1 桶锁机制

每个内存大小桶独立一把锁,而非全局锁:

cpp 复制代码
class SpanList {
    std::mutex _mtx;  // 每个桶独立锁
    Span* _head;
};

SpanList _spanList[NFREE_LISTS];  // 208 个桶,208 把锁

桶锁优势:

  • 全局锁:所有分配串行排队,并发极差;
  • 桶锁:不同大小对象分配并行进行,锁冲突概率极低。

4.2 获取 Span:GetOneSpan

当线程缓存桶空时,中心缓存负责提供可用 Span,无可用 Span 则向页缓存申请:

cpp 复制代码
Span* CentralCache::GetOneSpan(SpanList& list, size_t size) {
    // 1. 查找当前桶内有空闲对象的 Span
    Span* it = list.Begin();
    while (it != list.End()) {
        if (it->_freeList != nullptr)
            return it;
        it = it->_next;
    }

    // 2. 解锁,向 PageCache 申请
    list._mtx.unlock();
    PageCache::GetInstance()->_pageMtx.lock();
    Span* span = PageCache::GetInstance()->NewSpan(
        SizeClass::NumMovePage(size)
    );
    PageCache::GetInstance()->_pageMtx.unlock();

    // 3. 切分 Span 为小对象链表
    char* start = (char*)(span->_pageId << PAGE_SHIFT);
    size_t bytes = span->_n << PAGE_SHIFT;
    char* end = start + bytes;

    span->_freeList = start;
    start += size;
    void* tail = span->_freeList;

    while (start < end) {
        NextObj(tail) = start;
        tail = NextObj(tail);
        start += size;
    }
    NextObj(tail) = nullptr;

    // 4. 重新加锁,将 Span 挂入桶
    list._mtx.lock();
    list.PushFront(span);
    return span;
}

4.3 批量获取:FetchRangeObj

从 Span 中批量截取一段自由链表,返回给 ThreadCache:

cpp 复制代码
size_t CentralCache::FetchRangeObj(
    void*& start, void*& end,
    size_t batchNum, size_t size
) {
    size_t index = SizeClass::Index(size);
    _spanList[index]._mtx.lock();

    Span* span = GetOneSpan(_spanList[index], size);
    assert(span && span->_freeList);

    // 批量取对象
    start = span->_freeList;
    end = start;
    size_t actualNum = 1;

    while (actualNum < batchNum && NextObj(end) != nullptr) {
        end = NextObj(end);
        actualNum++;
    }

    // 截断链表
    span->_freeList = NextObj(end);
    NextObj(end) = nullptr;
    span->_useCount += actualNum;

    _spanList[index]._mtx.unlock();
    return actualNum;
}

五、第三层:PageCache(页缓存)

PageCache 是最底层,管理以页为单位的大块内存,负责与系统交互并合并碎片。

5.1 核心职责

  1. 管理页级 Span;
  2. 向系统申请 / 释放大块内存;
  3. Span 前后合并,消除外碎片;
  4. 提供页号到 Span 的极速映射。

5.2 申请 Span:NewSpan

cpp 复制代码
Span* PageCache::NewSpan(size_t k) {
    assert(k > 0);

    // 1. 超大页直接向系统申请
    if (k > NPAGES - 1) {
        void* ptr = SystemAlloc(k);
        Span* span = spanPool.New();
        span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
        span->_n = k;
        return span;
    }

    // 2. 对应桶有直接返回
    if (!_spanLists[k].Empty()) {
        return _spanLists[k].PopFront();
    }

    // 3. 更大 Span 切分
    for (size_t i = k + 1; i < NPAGES; i++) {
        if (!_spanLists[i].Empty()) {
            Span* nspan = _spanLists[i].PopFront();

            Span* kspan = spanPool.New();
            kspan->_pageId = nspan->_pageId;
            kspan->_n = k;

            nspan->_pageId += k;
            nspan->_n -= k;

            _spanLists[nspan->_n].PushFront(nspan);
            return kspan;
        }
    }

    // 4. 无空闲内存,向系统申请 128 页
    Span* NewBigSpan = spanPool.New();
    void* ptr = SystemAlloc(NPAGES - 1);
    NewBigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
    NewBigSpan->_n = NPAGES - 1;
    _spanLists[NewBigSpan->_n].PushFront(NewBigSpan);

    return NewSpan(k);
}

5.3 Span 合并:ReleaseSpanToPagecache

合并是降低外碎片的关键,释放 Span 时尝试与前后空闲 Span 合并:

cpp 复制代码
void PageCache::ReleaseSpanToPagecache(Span* span) {
    if (span->_n > NPAGES - 1) {
        SystemFree((void*)(span->_pageId << PAGE_SHIFT));
        return;
    }

    // 向前合并
    while (true) {
        PAGE_ID preid = span->_pageId - 1;
        Span* prespan = _idSpanMap.get(preid);

        if (!prespan || prespan->_isuse) break;
        if (prespan->_n + span->_n > NPAGES-1) break;

        span->_pageId = prespan->_pageId;
        span->_n += prespan->_n;
        _spanLists[prespan->_n].Erase(prespan);
        spanPool.Delete(prespan);
    }

    // 向后合并
    while (true) {
        PAGE_ID nextid = span->_pageId + span->_n;
        Span* nextspan = _idSpanMap.get(nextid);

        if (!nextspan || nextspan->_isuse) break;
        if (nextspan->_n + span->_n > NPAGES-1) break;

        span->_n += nextspan->_n;
        _spanLists[nextspan->_n].Erase(nextspan);
        spanPool.Delete(nextspan);
    }

    _spanLists[span->_n].PushFront(span);
    span->_isuse = false;
}

六、关键优化:基数树

页号到 Span 的映射是释放流程的关键路径,性能直接影响整体效率。

6.1 为什么弃用 unordered_map

  • 哈希冲突可能导致退化为 O (n);
  • 节点离散,缓存命中率低;
  • 全局锁竞争严重,高并发瓶颈明显。

6.2 基数树(Radix Tree)原理

采用两层基数树,通过位分割实现真正 O (1) 查找:

复制代码
PAGE_ID 结构(32 位 - PAGE_SHIFT = 19 位):
┌─────────────────┬───────────┐
│   高 11 位       │  低 8 位  │
│  (根节点索引)     │ (叶子索引)│
└─────────────────┴───────────┘

查找逻辑:

cpp 复制代码
void* get(Number k) {
    Number i1 = k >> LEAF_BITS;
    Number i2 = k & (LEAF_LENGTH-1);

    if (root_[i1] == nullptr) return nullptr;
    return root_[i1]->values[i2];
}

优势:

  • 纯位运算,无哈希计算;
  • 无冲突,稳定 O (1);
  • 内存连续,缓存友好;
  • 支持无锁读取,并发性能极强。

七、完整数据流图

7.1 分配流程

复制代码
用户调用 ConcurrentAlloc(size)
           │
           ▼
      size <= 256KB?
           │
      ┌────┴────┐
      │ 是      │ 否
      ▼         ▼
 ThreadCache   PageCache
      │         │
      ▼         │
  桶有空间?     │
   │ 是  │ 否  │
   ▼     ▼     │
  pop  Fetch   │
        │      │
        ▼      │
  CentralCache  │
        │      │
        ▼      │
   桶有Span?   │
    │ 是  │ 否 │
    ▼     ▼    │
   取N个  NewSpan
           │    │
           ▼    │
        PageCache
              │
              ▼
         系统申请

7.2 释放流程

复制代码
用户调用 ConcurrentFree(ptr)
           │
           ▼
   通过地址算页号
   PageCache._idSpanMap.get(页号)
           │
           ▼
        找到 Span
           │
           ▼
      Span._useCount--
           │
           ▼
      useCount == 0?
           │
      ┌────┴────┐
      │ 是      │ 否
      ▼         │
 还给PageCache   │ 完成
      │         │
      ▼         │
  向前合并? ──→ 相邻Span空闲?
      │         │
      ▼         │
  向后合并? ──→ 相邻Span空闲?
      │         │
      ▼         │
  挂回PageCache桶

八、性能测试

测试环境

  • 编译器:GCC 13.2.0 (MinGW-W64)
  • 优化级别:-O2
  • 场景:4 线程,10 轮,每轮 10000 次分配 / 释放

测试结果

指标 ConcurrentAlloc malloc 提升
Alloc (10 万次) 33 ms 87 ms 2.6x
Dealloc (10 万次) 31 ms 87 ms 2.8x
总计 64 ms 174 ms 2.7x

性能提升来源

  • 基数树替代哈希表:核心性能提升点;
  • ThreadCache 无锁:绝大多数操作无同步开销;
  • 桶锁:大幅降低锁竞争;
  • 慢启动 + 批量操作:减少系统调用与锁次数。

九、核心设计思想总结

9.1 分层解耦

  • ThreadCache:无锁快速分配
  • CentralCache:批量调度、桶锁并行
  • PageCache:页管理、碎片合并

9.2 缓存友好

连续内存结构 + LIFO 策略,最大化 CPU 缓存命中率。

9.3 反馈调节

慢启动算法自适应业务负载,平衡速度与内存占用。

9.4 空间换时间

多层缓存与基数树,用可控内存开销换取极致性能。


十、未来改进方向

  1. 对基数树实现读写锁,进一步提升并发读性能;
  2. 扩大 ThreadCache 容量,减少跨层交互;
  3. 支持大页内存(hugetlb),降低 TLB miss;
  4. 集成内存泄漏检测与堆分析工具;
  5. 支持多 NUMA 节点感知分配。

项目完整代码可移步 Gitee仓库:高并发内存池查看。

相关推荐
Mr_Xuhhh2 小时前
LeetCode 热题 100 刷题笔记:数组与排列的经典解法
数据结构·算法·leetcode
学以智用2 小时前
.NET Core 部署上线完整教程(Windows IIS / Linux / Docker)
后端·.net
爱丽_2 小时前
AQS 的 CLH 同步队列:入队/出队、park/unpark 与“公平性”从哪来
java·开发语言·jvm
黄昏恋慕黎明2 小时前
spring的IOC与DI
java·后端·spring
千里马学框架2 小时前
aospc/c++的native 模块VScode和Clion
android·开发语言·c++·vscode·安卓framework开发·clion·车载开发
用户608186527902 小时前
WPF 命令 ICommand 从原理到实战
后端
liuqun03192 小时前
go进阶之gc
开发语言·后端·golang
李小狼lee2 小时前
以一个简单案例来讲解RAG
后端
程序员清风2 小时前
OpenAI创始人学AI的底层逻辑,普通人照着做就能上手!
java·后端·面试