直接new/delete在高频小对象场景下变慢,因堆分配器需维护空闲链表、处理碎片、加锁(多线程时),甚至触发系统调用;内存池通过预分配大块内存并自行管理小块复用绕过堆管理器。

为什么直接 new/delete 在高频小对象场景下会变慢
因为堆分配器要维护空闲链表、处理碎片、加锁(多线程时),每次 new 都可能触发系统调用或复杂查找。小对象(比如几十字节的节点)反复申请释放,开销远超对象本身。
内存池本质是「预分配一大块内存 + 自己管理其中的小块复用」,绕过堆管理器。但别一上来就写通用池——先从固定大小对象池开始,这是最稳、最容易控制的起点。
- 避免用
std::vector<char></char>存储池内存:它可能重新分配,导致原有指针失效;改用std::unique_ptr<char></char>或operator new手动申请 - 不要在池里存构造/析构逻辑:C++ 对象需要显式调用
placement new和obj->~T(),否则资源泄漏或未定义行为 - 单线程池别加锁;多线程场景优先用线程局部池(
thread_local),比全局锁快得多
怎么写一个线程安全的 fixed-size 内存池
核心是三部分:内存块(raw memory)、空闲链表(free list)、原子计数器(可选)。不用红黑树或伙伴系统,就用单向链表把空闲块串起来,头插头取,O(1)。
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
class FixedPool {
char* _memory;
std::atomic<void*> _free_list{nullptr};
size_t _block_size;
size_t _block_count;
<p>public:
FixedPool(size_t block_size, size_t count)
: _block_size{block_size}, _block_count{count} {
_memory = static_cast<char<em>>(operator new(block_size </em> count));
// 构建初始空闲链表:每个块头存下一个空闲地址
for (size_t i = 0; i < count - 1; ++i) {
auto ptr = _memory + i <em> block_size;
</em>static_cast<void<strong>>(ptr) = _memory + (i + 1) <em> block_size;
}
</em>static_cast<void</strong>>(_memory + (count - 1) * block_size) = nullptr;
_free_list.store(_memory);
}</p><pre class='brush:php;toolbar:false;'>void* allocate() {
void* expected;
void* desired;
do {
expected = _free_list.load();
if (!expected) return nullptr;
desired = *static_cast<void**>(expected);
} while (!_free_list.compare_exchange_weak(expected, desired));
return expected;
}
void deallocate(void* p) {
void* expected;
do {
expected = _free_list.load();
*static_cast<void**>(p) = expected;
} while (!_free_list.compare_exchange_weak(expected, p));
}};
-
compare_exchange_weak必须循环使用,失败后要重读_free_list,否则链表断裂 - 每个空闲块前
sizeof(void*)字节被用作 next 指针,所以实际可用空间 =_block_size - sizeof(void*),分配时注意对齐 - 不支持
malloc那样的任意尺寸分配;如果需要多种尺寸,得维护多个池(比如 32B/64B/128B 各一个)
什么时候该放弃自研内存池
当你的对象生命周期不规则、尺寸差异大、或者总分配次数不高(比如每秒不到几千次),自研池反而增加复杂度和 bug 风险。
更现实的选择:
- 先用
std::pmr::monotonic_buffer_resource(C++17):适合短生命周期、顺序分配+一次性释放的场景(如解析一帧数据) - 调试阶段打开
ASAN和UBSAN:内存池极易出现 use-after-free、double-free、未调用析构函数等问题,没工具护航等于裸奔 - 生产环境怀疑分配瓶颈?先跑
perf record -e syscalls:sys_enter_brk,syscalls:sys_enter_mmap看是否真卡在系统调用上,而不是误判热点
placement new 和对象生命周期怎么不出错
内存池只管内存,不管对象。你必须手动控制构造和析构,漏掉任何一个都会出问题——尤其是异常路径。
- 分配后必须用
new (ptr) T(args...)构造,不能直接static_cast<t>(ptr)</t> - 释放前必须显式调用
static_cast<t>(ptr)->~T()</t>,否则T的析构逻辑(比如关闭文件、释放子资源)完全不会执行 - 如果
T构造抛异常,要立即把内存还给池,否则内存泄露;建议把构造逻辑包进 RAII 包装器里,避免裸指针流转 - 禁止对池中对象做
delete:它会尝试调用全局operator delete,而你的内存根本不是用new分配的
内存池真正的复杂点不在分配算法,而在与对象模型的耦合细节——特别是异常安全、多态对象(虚表指针)、对齐要求(比如 alignas(16) 类型)。这些地方一错就是 core dump,而且很难复现。









