在C++性能优化的世界里,小对象的内存分配一直是个让人头疼的问题。直接上固定大小内存池,确实能把分配和释放操作压到O(1)的常数级复杂度,听着就让人心动。但这里有个前提:对象大小真的固定,生命周期模式也要能预测。如果这两点不满足,那点性能收益,可能连维护成本都填不平。

为什么说 operator new 预分配比 new char[] 更安全?
预分配底层内存,关键一步是绕过构造调用,否则会触发未定义行为。用 operator new(size_t) 是正确姿势,它只申请原始内存,不碰任何构造逻辑。而 new char[N] 虽然也能用,但语义上属于“创建字符数组”,在极端优化场景或自定义全局 operator new 的情况下,很可能被拦截或重定向。这个坑,不少开发者踩过。
几个必须注意的细节:
- 块大小必须 ≥
sizeof(void*),否则连自由链表指针都存不下 - 建议对齐到 8 或 16 字节(
alignof(std::max_align_t)),这是为了躲开 x86 的性能惩罚,以及 ARM 上的硬件异常 - 预分配后别直接 cast 成目标类型指针——得先留出头部空间存 next 指针
空闲链表头:用 std::atomic 实现无锁栈
多线程环境下,如果给内存池加互斥锁,那性能优势基本就废了。用原子指针模拟单向栈,是目前开销最小的方案。分配时,CAS 读头→更新头为 next;释放时,写本块 next→CAS 更新头为本块。逻辑清晰,但执行起来有坑。
- 所有线程必须使用相同内存偏移存 next 指针(比如统一从块起始地址 + 0 开始),否则 CAS 会看到错乱地址,结果不敢想象
- 别用
std::shared_ptr管理池本身——析构顺序不可控,极易引发 use-after-free,这是血的教训 - 需要考虑的是,如果线程数 > 4 且竞争激烈,建议加一层 per-thread cache,避免所有线程争抢同一链表头,否则性能会大幅下降
对象构造和析构:必须手动配对 new(p) T() / p->~T()
内存池只负责管内存,不关心对象生命周期。漏掉任意一环,轻则资源泄漏,重则未定义行为。比如析构没调用,智能指针的控制块残留,程序迟早出问题。
- 构造前确保内存已对齐:
alignas(T) char buf[sizeof(T)]或用std::aligned_storage_t - 析构必须在
deallocate之前完成,否则deallocate可能复用该内存,导致新对象覆盖未析构旧对象的内部状态 - 若对象含虚函数或继承关系,析构顺序仍需符合 C++ 对象模型——池不改变这一规则
说实话,最难缠的不是写错链表逻辑,而是忘记 placement new 和手动析构这两步。它们不在编译器检查范围内,出问题时往往表现为偶发崩溃或数据错乱,调试成本远高于实现本身。所以,务必把这两步刻进代码习惯里。