先交代背景:我做C++服务端开发这些年,被内存分配卡到过不止一次。业务流量一上去,最不起眼的 new/delete、malloc/free 反而成了最大的热点。于是“自定义分配器”这个词频繁出现在我的优化清单里,从最早照着网上的代码抄对象池,到后来自己设计多线程版本,再到现在用一套固定流程做性能对比,踩过的坑、总结出的判断标准都挺值得聊聊。
这篇内容的核心就是“自定义分配器性能对比”这件事到底怎么做、数据怎么看、结论怎么下。我会用自己的实测方式拆一遍,同时把实现思路和常见陷阱写清楚。适合正在做网络服务器、游戏后端、嵌入式中间件,或者被 malloc 锁竞争逼到要看底层分配器的朋友。哪怕你现在不准备手写分配器,搞懂这套对比方法也能帮你判断:tcmalloc、jemalloc 或者简单内存池,到底值不值得引入。
1. 自定义分配器到底解决什么问题
动手之前,先得搞清楚默认分配器哪里不给力,否则容易把力气用错地方。很多人的第一反应是“malloc 不够快”,但“不够快”是个很模糊的描述。你要能够回答:是单次延迟太高,还是多线程时吞吐上不去,还是内存碎片太严重,还是峰值内存超了。不同的瓶颈,对应不同的分配器方案。
1.1 默认分配器的开销藏在哪儿
glibc 的 malloc 实现是 ptmalloc 思路,为了兼顾任意大小、任意线程模型、任意生命周期,它维护了多个 arena,线程多的时候还得做 arena 的分配和锁管理。单线程场景下,一次 malloc/free 可能只有几十纳秒,看着不大,但如果是高频交易里的撮合循环、游戏服务器里的每帧对象创建,一次逻辑往往会产生几百上千次分配,这个开销会被放大得非常明显。
new/delete 更不用说,本质上就是对 operator new 的封装,底层还是 malloc/free。所以对齐要求、元数据开销、锁竞争、系统调用这些成本,一样都没少。内存释放时,free 不一定把页面还给操作系统,而是留在进程的堆缓存里,这就导致两个结果:一是进程的 RSS 看起来一直很高,二是堆里的空闲块可能被切得很碎,后续分配变慢。
自定义分配器要解决的,往往就是这几件事里的某一两件。比如固定大小对象的场景,可以用对象池直接把 free list 串好,分配就是取一个头部节点,释放就是塞回头部,完全绕开 malloc 的复杂管理。再比如生命周期一致的批量对象,可以用 arena,一次性申请大块内存,用偏移量切,结束统一释放。核心思路是“用局部规则换掉通用逻辑”,从而减少锁竞争、减少系统调用、减少元数据。
1.2 三类值得自己做分配器的场景
第一类是高频小对象场景,典型代表是网络收发包、连接对象、协议编解码缓冲区。这些对象大小固定、生命周期短、创建销毁频繁,对象池基本是满分答案。第二类是批次生命周期场景,比如一帧游戏里的临时碰撞数据、一次请求处理的中间结果,它们在同一批逻辑内创建,逻辑结束就不需要了,用 arena 释放整块内存,代价极低。第三类是跨线程传递比较频繁的场景,自己设计带线程局部缓存的分配器,可以明显降低全局锁的争用。
反过来说,如果你的业务内存对象大小差别巨大,生命周期嵌套复杂,甚至对象之间互相持有引用,那我建议先别碰自定义分配器。硬写出来的分配器可能比 malloc 更慢,因为你设计不出足够通用的规则,反而要花大量 CPU 去处理边界情况。所以做性能对比之前,先确定场景是否值得。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 可选方案:从 std::allocator 到手写内存池
经常有人把 std::allocator 和“自定义分配器”混在一起说。实际上 std::allocator 只是一个标准容器和内存分配之间的接口层,它默认的实现就是调用 operator new / operator delete,并没有带来性能提升。你真正要做的,是往这个接口里塞入自己设计的存储分配策略。
2.1 std::allocator 只是入口,不是银弹
C++ 标准库容器都支持传入模板参数 Allocator,例如 std::vector<int, MyAllocator
使用 std::allocator 接口需要实现 allocate/deallocate,规范里还要求 value_type、rebind、相等比较等。C++20 之后有 std::allocator_traits,写起来简单不少,但核心仍是 allocate 返回原始内存、deallocate
