new/delete在高频场景下拖慢程序,因其每次调用均需堆管理器查链表、合碎片、加锁、更新元数据,小对象短生命周期时易成瓶颈,多线程下全局堆锁更导致吞吐骤降。

为什么 new/delete 在高频场景下会拖慢程序
频繁调用 new 和 delete 本质是和堆管理器(如 malloc/free)打交道,每次都要查空闲链表、合并碎片、加锁(多线程下)、更新元数据。不是“申请几字节就拿几字节”,而是常伴随几十甚至上百字节的额外开销。尤其当对象小(比如 std::shared_ptr 或自定义消息结构体)、生命周期短、每秒创建销毁数万次时,堆分配器很快成为瓶颈,top 看到 CPU 花在 malloc_consolidate 或 arena_get2 上就很典型。
- 对象大小越接近 16/32/64 字节这种对齐边界,glibc malloc 的 fastbin 利用率越高;但一旦跨过阈值或出现并发争抢,性能断崖下跌
- 多线程下若共用全局堆,
malloc 内部锁会串行化所有分配请求,实测 8 线程可能只跑出单线程 1.2 倍吞吐
-
operator new 默认不保证返回地址缓存行对齐,多个小对象挤在同一 cache line 里会引发 false sharing
手写简易对象池:绕过堆管理器的关键三步
核心思路不是“预分配一大块内存”,而是“自己管内存复用”——把已释放的对象挂进自由链表,下次直接复位 + 返回指针,完全跳过 malloc。
template<typename T>
class ObjectPool {
struct Node { Node* next; };
Node* free_list_ = nullptr;
std::vector<std::unique_ptr<char[]>> chunks_;
static constexpr size_t kChunkSize = 4096;
<p>public:
T<em> acquire() {
if (!free<em>list</em>) {
// 批量申请,减少系统调用次数
auto chunk = std::make<em>unique<char[]>(kChunkSize);
chunks</em>.push_back(std::move(chunk));
char</em> base = chunks_.back().get();
for (size_t i = 0; i < kChunkSize; i += sizeof(T)) {
auto node = reinterpret_cast<Node<em>>(base + i);
node->next = free<em>list</em>;
free<em>list</em> = node;
}
}
auto node = free<em>list</em>;
free<em>list</em> = node->next;
return reinterpret_cast<T</em>>(node);
}</p><pre class='brush:php;toolbar:false;'>void release(T* ptr) {
auto node = reinterpret_cast<Node*>(ptr);
node->next = free_list_;
free_list_ = node;
}
malloc 内部锁会串行化所有分配请求,实测 8 线程可能只跑出单线程 1.2 倍吞吐 operator new 默认不保证返回地址缓存行对齐,多个小对象挤在同一 cache line 里会引发 false sharing malloc。
template<typename T>
class ObjectPool {
struct Node { Node* next; };
Node* free_list_ = nullptr;
std::vector<std::unique_ptr<char[]>> chunks_;
static constexpr size_t kChunkSize = 4096;
<p>public:
T<em> acquire() {
if (!free<em>list</em>) {
// 批量申请,减少系统调用次数
auto chunk = std::make<em>unique<char[]>(kChunkSize);
chunks</em>.push_back(std::move(chunk));
char</em> base = chunks_.back().get();
for (size_t i = 0; i < kChunkSize; i += sizeof(T)) {
auto node = reinterpret_cast<Node<em>>(base + i);
node->next = free<em>list</em>;
free<em>list</em> = node;
}
}
auto node = free<em>list</em>;
free<em>list</em> = node->next;
return reinterpret_cast<T</em>>(node);
}</p><pre class='brush:php;toolbar:false;'>void release(T* ptr) {
auto node = reinterpret_cast<Node*>(ptr);
node->next = free_list_;
free_list_ = node;
}};
- 必须用
reinterpret_cast而非static_cast,因为Node和T无继承关系 -
chunks_存的是char[]原始内存块,避免对T构造/析构的干扰;对象生命周期由业务代码控制 - 每次
acquire()不做构造,release()不做析构——这是和std::pmr::polymorphic_allocator的关键区别,也意味着你得自己确保T的构造函数在acquire()后显式调用
std::pmr::monotonic_buffer_resource 为什么不适合高频回收场景
它适合“一次分配、多次使用、最后统一释放”的模式(比如解析一帧数据),但不支持单个对象归还。只要调用过 allocate(),对应内存直到整个 monotonic_buffer_resource 销毁才释放。
- 若误用
std::pmr::polymorphic_allocator 配合 monotonic_buffer_resource 做对象池,deallocate() 实际是空操作,内存只增不减
- 它底层依赖
std::pmr::memory_resource::do_allocate,最终仍走 malloc,没绕过堆管理器
- C++17 起部分 STL 容器(如
std::pmr::vector)支持传入 allocator,但 std::pmr::string 内部仍可能触发小字符串优化外的堆分配,不可控
对象池上线前必须检查的三个坑
- sizeof(T) 必须大于等于 sizeof(Node*)(通常 8 字节),否则自由链表节点塞不下指针;小于时得用联合体或外部索引数组
- 多线程访问必须加锁或改用无锁链表(如 std::atomic<node></node> + CAS),裸用 free_list_ 指针在多核下必然崩溃
- 对象析构逻辑不能依赖 ~T() 自动触发——release() 只归还内存,业务层得在归还前手动调用 ptr->~T(),否则资源泄漏(比如文件描述符、GPU buffer handle)
std::pmr::polymorphic_allocator 配合 monotonic_buffer_resource 做对象池,deallocate() 实际是空操作,内存只增不减 std::pmr::memory_resource::do_allocate,最终仍走 malloc,没绕过堆管理器 std::pmr::vector)支持传入 allocator,但 std::pmr::string 内部仍可能触发小字符串优化外的堆分配,不可控 sizeof(T) 必须大于等于 sizeof(Node*)(通常 8 字节),否则自由链表节点塞不下指针;小于时得用联合体或外部索引数组
- 多线程访问必须加锁或改用无锁链表(如 std::atomic<node></node> + CAS),裸用 free_list_ 指针在多核下必然崩溃
- 对象析构逻辑不能依赖 ~T() 自动触发——release() 只归还内存,业务层得在归还前手动调用 ptr->~T(),否则资源泄漏(比如文件描述符、GPU buffer handle)
C++ 对象池真正的复杂点不在分配逻辑,而在于谁负责构造/析构、内存对齐是否满足 SIMD 指令要求、以及和现有 RAII 习惯的冲突。很多团队卡在“以为用了池就万事大吉”,结果发现 std::shared_ptr 的控制块还在疯狂 new。











