直接 new/delete 不适合高频小对象分配,因其引发堆锁竞争、内存碎片、缓存不友好及元数据开销大;固定长度内存池通过预分配+空闲链表+原子操作解决,提升吞吐与局部性。

为什么直接 new/delete 不适合高频小对象分配
频繁调用 new 和 delete 会触发堆管理器加锁、内存碎片、缓存不友好等问题。尤其在每秒百万级小对象(如 16–128 字节)的场景下,malloc 的元数据开销和查找成本会明显拖慢吞吐量。
- 标准堆分配器通常为每个块附加 8–16 字节元数据,对小对象而言占比过高
- 多线程下
malloc内部锁竞争严重,pthread_mutex_t成为瓶颈 - CPU 缓存行(64 字节)被跨对象分割,降低访问局部性
如何设计单类型固定长度内存池(以 32 字节为例)
核心是预分配大块内存 + 单向空闲链表 + 无锁(单线程)或轻量原子操作(多线程)。不依赖虚函数或模板特化开销,避免 RTTI。
- 用
std::aligned_storage_t或alignas(32) char确保对齐,避免std::malloc返回地址不满足要求 - 空闲链表头用
std::atomic<char></char>存储指向首个空闲块的指针,每个块前 8 字节存下一个空闲块地址 - 初始化时将整块内存按 32 字节切分,反向链接成链表(避免首次分配时遍历)
- 分配:原子读取头指针 → 更新头指针为下一个地址 → 返回当前地址
- 释放:原子写入当前块的 next 字段为原头指针 → 原子更新头指针为当前块地址
示例关键片段:
class FixedPool32 {
std::atomic<char*> free_head_;
char* mem_block_;
size_t block_size_;
<p>public:
FixedPool32(size_t n) : block<em>size</em>(n <em> 32) {
mem<em>block</em> = static_cast<char</em>>(std::aligned_alloc(32, block<em>size</em>));
free<em>head</em>.store(mem<em>block</em>);
// 反向链接:从末尾开始,每个块前 8 字节存前一个块地址
for (size_t i = n; i > 1; --i) {
char<em> p = mem<em>block</em> + (i - 1) </em> 32;
*reinterpret_cast<char*<em>>(p) = p - 32;
}
</em>reinterpret_cast<char**>(mem<em>block</em>) = nullptr;
}</p><pre class='brush:php;toolbar:false;'>void* allocate() {
char* expected = free_head_.load();
do {
if (!expected) return nullptr;
} while (!free_head_.compare_exchange_weak(expected, *reinterpret_cast<char**>(expected)));
return expected;
}
void deallocate(void* p) {
char* ptr = static_cast<char*>(p);
char* expected = free_head_.load();
do {
*reinterpret_cast<char**>(ptr) = expected;
} while (!free_head_.compare_exchange_weak(expected, ptr));
}};
立即学习“C++免费学习笔记(深入)”;
多线程下要注意的三个原子陷阱
compare_exchange_weak 失败重试逻辑必须显式处理 ABA 问题——但固定长度池中只要不回收后立即重用同一地址(即不把内存 mmap(MAP_ANONYMOUS) 后 madvise(DONTNEED)),ABA 实际不会发生。真正易错点在:
- 忘记对
free_head_初始化为nullptr或合法地址,导致首次allocate()解引用野指针 - 释放时未保证
p确实来自本池(调试可用assert(p >= mem_block_ && p ) - 不同大小的池共用同一块内存却没隔离边界,造成链表指针越界覆盖(例如 32 字节池误把 64 字节块当 32 字节链表节点)
何时该换用 std::pmr::monotonic_buffer_resource 或 tbb::scalable_allocator
如果你的“固定长度”只是近似(比如对象实际大小在 24–40 字节浮动),或者需要支持多种尺寸混合分配,硬写池反而增加维护成本和 bug 风险。
-
std::pmr::monotonic_buffer_resource适合短生命周期、只分配不释放的场景(如一帧渲染对象),但不支持单个对象释放 -
tbb::scalable_allocator在多核下比 glibc malloc 快 2–5 倍,且无需改代码,export TBB_MALLOC_DISABLE_FALLBACK=1可避免回退到系统 malloc - Linux 上
malloc使用MAP_HUGETLB分配 2MB 大页能显著减少 TLB miss,比手写池更省事
自己写池的收益集中在:确定单一尺寸、对象生命周期高度一致、且 profiler 明确指出堆分配占 CPU >15% 的模块。其他情况,先压测再动手。



















