直接 new/delete 在高频小对象场景下性能差,因每次调用触发系统调用和堆元数据维护开销,远超构造函数本身;固定大小内存池通过 union 复用内存头实现零开销空闲链表,配合 placement new 与显式析构管理对象生命周期,并采用 TLS + 批量搬运提升线程安全吞吐。

为什么直接 new/delete 在高频小对象场景下会拖垮性能
因为每次 new 都触发系统调用,陷入内核态;delete 不仅要释放内存,还要维护堆管理元数据(如空闲块链表、合并逻辑)。对 16–128 字节的小对象来说,这部分开销可能比构造函数本身还高 5 倍以上。更糟的是,长期运行后,malloc 管理的堆会散布大量无法复用的细碎空闲块——这不是你代码写的 bug,是通用分配器的固有局限。
固定大小内存池的核心实现:用 union 复用内存头做 free list
关键不是“多 fancy 的类设计”,而是如何零额外开销地维护空闲块链表。最轻量的做法是把每个空闲块的前 sizeof(void*) 字节当作指针用:
union MemoryBlock {
char data[blockSize];
MemoryBlock* next;
};这样:
• 分配时:取 free_list_ 指向的块,更新 free_list_ = free_list_->next,返回 &block->data
• 释放时:把用户传入的 ptr 强转为 MemoryBlock*,填入 next 字段,再插回链表头
• 完全不额外 malloc 元数据,也不依赖 std::list 这类带节点开销的容器
placement new + 显式析构才是对象生命周期的正确打开方式
内存池只管内存,不管对象。漏掉这步,对象没构造或没析构,等于白写:
立即学习“C++免费学习笔记(深入)”;
- 分配后必须用
new (ptr) T(args...)触发构造,不能直接static_cast<T*>(ptr) - 释放前必须先调
static_cast<T*>(ptr)->~T(),再把ptr还给池子 - 如果对象有虚函数或非平凡析构,跳过析构会导致资源泄漏(如文件句柄、socket)
- 模板化池建议用
std::is_trivially_destructible_v<T>分支优化:平凡类型可跳过析构调用
线程安全别硬上 mutex,TLS + 批量搬运才是实用解法
单个全局锁会让所有线程在 allocate() 上排队,吞掉 90% 的性能收益。真实项目里更有效的组合是:
- 每个线程持有一个
thread_local static FixedSizeMemoryPool<Node> local_pool; - 本地池空了,才向中央池(用
std::atomic<Node*>+ CAS 实现无锁栈)批量申请 64 个块 - 归还时优先塞本地池,满额后再推 32 个回中央池
- 完全避开互斥锁,CAS 失败就重试,实测在 32 核机器上吞吐提升 4.7 倍
真正难的不是写一个能跑的池,而是让每个线程的局部池容量、向中央池搬运的阈值、以及 block size 的分级粒度,都贴合你实际 profile 出来的分配分布——这些参数一旦写死,就很难再动态调优。


















