内存池核心设计:固定大小块分配器、空闲链表、内存碎片消除

好,咱们进入正题。这一章讲的是内存池最核心的三个东西:固定大小块分配器、空闲链表、还有内存碎片消除。说白了,这就是内存池的骨架。你把这几个搞明白了,后面那些花哨的优化才有根基。

我个人习惯,设计任何系统都先问自己一个问题:我要解决什么痛点? 高频交易里,内存分配最烦人的就是两件事:一是慢,二是碎。慢是因为系统调用(malloc/free)有锁,碎是因为频繁申请释放导致内存像蜂窝煤一样。内存池就是来收拾这个烂摊子的。

固定大小块分配器

先讲固定大小块。这个思路其实很简单:既然你高频交易里用的对象大小都差不多(比如订单、报价、成交记录),那我干脆提前切好一堆固定大小的块,你来了直接拿,用完还回来。

我在项目中遇到过一种情况:有人想搞通用内存池,支持任意大小分配。结果呢?性能反而比 malloc 还差。为什么?因为通用方案要处理各种对齐、合并、分裂,复杂度上去了,延迟就下不来。所以我的建议是:别贪心,固定大小就是最快的。

来看一个最简单的实现:

// 固定大小块分配器核心结构
struct FixedBlockAllocator {
    char* pool;          // 预分配的大块内存
    size_t block_size;   // 每个块的大小(比如64字节)
    size_t block_count;  // 块的总数
    void* free_list;     // 空闲链表头
};

这里有个关键点:block_size 怎么选? 我一般会取对象大小的上对齐到 8 或 16 字节。比如你的 Order 结构体是 38 字节,那就对齐到 48 字节。为什么?因为 CPU 喜欢对齐访问,不对齐会罚站(性能惩罚)。

小技巧: 如果你不确定对象大小,可以用 sizeof() 打印出来看看。我见过有人凭感觉设了 64 字节,结果对象实际才 20 字节,白白浪费了 2/3 的内存。

空闲链表

接下来是空闲链表。这是内存池的「调度中心」。每个空闲块里存一个指针,指向下一个空闲块。这样你就不需要额外的数据结构来管理空闲块了——块本身就成了链表节点。

你想想看,这多巧妙?内存块在被分配出去之前,里面存的是下一个空闲块的地址。分配出去后,这块内存就归用户用了。等用户归还时,我们再把它插回链表头部。

核心操作就两个:

  1. 分配(allocate): 从链表头取一个块,头指针后移。
  2. 释放(deallocate): 把块插回链表头,更新头指针。

代码实现大概长这样:

void* allocate(FixedBlockAllocator* alloc) {
    if (alloc->free_list == nullptr) {
        return nullptr; // 内存耗尽,需要扩容
    }
    void* block = alloc->free_list;
    alloc->free_list = *((void**)alloc->free_list);
    return block;
}

void deallocate(FixedBlockAllocator* alloc, void* block) {
    *((void**)block) = alloc->free_list;
    alloc->free_list = block;
}

嗯,这里要注意:分配和释放都是 O(1) 的。 没有遍历,没有排序,没有锁(单线程下)。这就是高频交易要的速度。

警告: 多线程环境下,这个简单的链表需要加锁或用原子操作。我曾经在一个项目里忘了加锁,结果两个线程同时分配,拿到了同一个块,数据全乱了。排查了整整一个下午。

内存碎片消除

最后聊内存碎片。这是内存池的「终极目标」。固定大小块分配器天然就能消除外部碎片——因为所有块一样大,不存在「大块被切成小块,小块释放后拼不回大块」的问题。

但是,内部碎片还是有的。 比如你的对象是 38 字节,块大小是 48 字节,那 10 字节就浪费了。这就是内部碎片。怎么处理?

  • 方法一: 精确匹配对象大小。比如你只有一种对象,那就把块大小设成 sizeof(Object)。
  • 方法二: 多级大小池。比如 32 字节、64 字节、128 字节各一个池,按需分配。这就是后面要讲的「伙伴系统」的雏形。
  • 方法三: 接受内部碎片。在高频交易里,延迟比内存更重要。多浪费几个字节,换来 O(1) 分配,值了。

我个人倾向方法三。为什么?因为高频交易场景下,内存通常不是瓶颈。你一个订单才几十字节,一秒钟几百万笔订单也就几十 MB。相比延迟抖动,这点浪费完全可以接受。

核心结论: 固定大小块 + 空闲链表 = 零外部碎片 + O(1) 分配释放。这是内存池最经典的组合,没有之一。

我曾经在一个低延迟交易系统里,把原来的 malloc/free 换成了固定大小块分配器。结果呢?平均分配延迟从 200 纳秒降到了 20 纳秒,而且没有一次超过 50 纳秒的抖动。这就是内存池的威力。

最后,我用一张图来总结这一章的核心逻辑:

内存池核心设计:固定大小块分配器 + 空闲链表 内存池 (预分配大块内存) 空闲链表头 (free_list) 块1 块2 块3 null 已分配块 (被用户使用中) 分配时从空闲链表头部取,释放时插回头部 分配: O(1) 从链表头取 释放: O(1) 插回链表头 外部碎片: 零 内部碎片: 可接受 (对齐浪费)

这张图把整个流程串起来了:预分配的大块内存被切成固定大小的块,空闲块通过链表串起来。分配时从头部取,释放时插回头部。整个过程没有碎片,没有遍历,没有系统调用。

好了,这一章的核心就这些。记住一句话:固定大小 + 空闲链表 = 高性能内存分配的基石。 下一章我们会在这个基础上,加入线程局部缓存和批量操作,让性能再上一个台阶。