malloc和new在多线程下变慢是因为ptmalloc等默认分配器需争抢全局arena锁;改用tcmalloc/jemalloc可启用线程局部缓存减少竞争;std::pmr::monotonic_buffer_resource适用于单次请求内单调分配、批量释放的场景。

为什么 malloc 和 new 在多线程下会变慢
因为默认的 libc 内存分配器(如 glibc 的 ptmalloc)在多线程场景下会对全局堆元数据加锁,尤其是高并发频繁申请小对象时,malloc 可能反复争抢同一个 arena 锁。这不是 C++ 语言问题,而是底层分配器实现决定的——每个线程并不天然拥有独立堆。
典型现象:压测时 CPU 利用率不高,但 perf record -e 'syscalls:sys_enter_brk,syscalls:sys_enter_mmap' 显示系统调用不多,而 perf record -e 'lock:lock_acquire' 却暴露出大量 arena 相关锁等待;或者 perf annotate malloc 发现热点卡在 __libc_malloc 内部的 arena_get2 或 arena_lock。
怎么让每个线程用独立内存池
最直接的方式是切换到线程局部缓存(TLS-based)分配器,比如 tcmalloc 或 jemalloc,它们默认启用 per-CPU/per-thread 的 cache 层,大幅减少锁竞争。
- 链接
tcmalloc:g++ -o app app.cpp -ltcmalloc(注意顺序,-ltcmalloc 放最后),启动时无需改代码 - 用
jemalloc:g++ -o app app.cpp -ljemalloc,也可通过LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./app注入 - 验证是否生效:运行时设置
MALLOC_CONF="stats_print:true"(jemalloc)或TCMALLOC_DEBUGLOG=1(tcmalloc),看日志中是否有 thread-local cache 命中信息
别手动写 TLS + std::vector 模拟内存池——除非你明确控制生命周期且对象大小固定,否则容易引入内存泄漏或跨线程释放风险。
立即学习“C++免费学习笔记(深入)”;
std::pmr::monotonic_buffer_resource 适合什么场景
它不解决锁竞争,但能彻底避开 malloc 调用——前提是你的对象生命周期满足“单调增长、批量释放”模型,比如一次请求处理中反复构造临时对象,结束后统一清空。
常见误用点:
- 把
monotonic_buffer_resource放在栈上,然后传给其他线程的polymorphic_allocator——这是未定义行为,buffer 生命周期和线程不匹配 - 混用不同
resource分配/释放同一块内存,比如用 A 的 allocator new,用 B 的 delete - 期望它提升随机分配/释放性能——它只对“先全量分配、再一次性释放”有效,中间不能有释放操作
示例节选:
std::pmr::monotonic_buffer_resource pool{1024*1024}; // 1MB buffer
std::pmr::vector<int> v{&pool};
v.reserve(10000);
for (int i = 0; i < 10000; ++i) v.push_back(i); // 全部从 pool 分配
// 函数结束时 pool 析构,整个 buffer 一次性归还
什么时候该怀疑是分配器问题而不是代码逻辑
当出现以下组合信号时,优先排查分配器:
- 单线程性能正常,线程数从 2 增加到 8 后吞吐量不升反降,甚至下降 30% 以上
- perf 火焰图里
__libc_malloc/je_malloc占比突增,且下方堆栈频繁出现pthread_mutex_lock或futex_wait - 用
valgrind --tool=massif发现峰值堆用量不大(比如 10⁶ 次/秒) - 替换为
LD_PRELOAD=./libdummy_malloc.so(一个只返回静态缓冲区的 dummy 分配器)后性能陡升——这基本锁定是分配器瓶颈
真正难的是混合场景:既有短生命周期小对象(适合 tcmalloc),又有大块长期驻留内存(可能触发 jemalloc 的 arena 扩缩抖动)。这时候得用 mallctl(jemalloc)或 TCMalloc_SetProfileInterval 动态调参,而不是只换一个库就以为万事大吉。


















