c++oding="utf-8" ?>
深度学习推理中malloc/free成为性能瓶颈,因其单次前向传播触发数百上千次小块分配,引发系统调用开销、arena锁竞争、内存碎片和TLB miss,导致P99延迟毛刺达2–5ms;关键在于将不可控的runtime分配转为可控、预置、无锁的复用。

为什么深度学习推理中 malloc/free 会成为性能瓶颈
深度学习推理常在单次前向传播中触发数百甚至上千次小块内存分配(比如中间 tensor 的 buffer、临时 workspace),而 malloc 在默认 glibc 实现下对小内存会走 brk 或 mmap,频繁调用会引发系统调用开销、锁竞争(多线程下 malloc 内部有 arena 锁)、内存碎片和 TLB miss。这不是“慢一点”的问题,而是会导致延迟毛刺(jitter)——比如 P99 延迟突然跳高 2–5ms,对实时服务非常致命。
关键不是“能不能避免分配”,而是“能不能把不可控的 runtime 分配,变成可控的、预置的、无锁的复用”。
用 std::pmr::monotonic_buffer_resource 快速构建只增不减的推理内存池
这是 C++17 引入的轻量级内存资源,适合单次推理生命周期:分配只向前推进,不回收单个块,整个池在推理结束时一次性释放。它天然无锁、零系统调用(底层可绑定到大块 mmap 或预分配 std::vector<char></char>)。
实操建议:
- 为每次推理准备一个独立的
std::pmr::monotonic_buffer_resource,生命周期与推理 session 对齐(例如封装进InferenceContext类成员) - 不要跨推理复用同一个
monotonic_buffer_resource,否则残留占用会累积;也不要把它设成全局单例 - 底层内存建议用
mmap(MAP_ANONYMOUS | MAP_PRIVATE)预分配 64MB–256MB(根据模型大小调整),避免首次分配触发 page fault 抖动 - 搭配
std::pmr::polymorphic_allocator使用,所有中间容器(如std::pmr::vector<float></float>、std::pmr::unordered_map)都显式传入该 allocator
// 示例:一次推理的内存上下文
class InferenceContext {
std::unique_ptr<char> pool_mem_;
std::pmr::monotonic_buffer_resource pool_;
std::pmr::polymorphic_allocator<float> alloc_;
<p>public:
InferenceContext(size_t pool_size = 128 <em> 1024 </em> 1024)
: pool<em>mem</em>(std::make_unique<char>(pool<em>size)),
pool</em>(pool<em>mem</em>.get(), pool<em>size),
alloc</em>(&pool_) {}</char></p>
<pre class="brush:php;toolbar:false;">template<typename t>
std::pmr::vector<t> make_vector() {
return std::pmr::vector<t>(alloc_);
}</t></t></typename>
};
遇到 std::bad_alloc 或越界踩内存?检查这三处
monotonic_buffer_resource 不做边界检查,也不扩容,一旦预分配空间耗尽就直接抛 std::bad_alloc;更危险的是,某些第三方库(如 ONNX Runtime 的某些 kernel)内部硬编码使用 new,完全绕过你的 pmr allocator。
排查要点:
- 在
monotonic_buffer_resource构造时传入 debug wrapper(比如自定义memory_resource记录每次do_allocate大小和累计用量),确认是否真超限,还是某次分配请求过大(>2MB)触发 fallback 到系统 malloc - 检查所有 tensor buffer 创建路径:是否每个
Tensor::data()都来自你控制的 allocator?ONNX Runtime 的Ort::Value默认不用 pmr,需用Ort::MemoryInfo指向你的内存区域 - 禁用编译器的
-fno-exceptions时,std::bad_alloc才能被捕获;否则可能静默 abort,看起来像段错误
需要真正“复用+回收”的场景?换 std::pmr::synchronized_pool_resource
monotonic_buffer_resource 适合单次推理;但如果你做 batch 推理(多个 input 共享 workspace)、或需要 pipeline 流水(prefill + decode 阶段复用 memory),就得支持“分配 → 使用 → 释放 → 再分配”。这时 std::pmr::synchronized_pool_resource 更合适:它按固定 size class 维护多个空闲链表,线程安全,释放后内存可被同 size 请求复用。
但要注意:
- 它比 monotonic 慢约 15–30%,因为有锁和链表操作;不过仍远快于系统 malloc
- size class 划分由实现决定(libstdc++ 和 libc++ 行为不同),大块内存(>128KB)可能直接 fallback 到 upstream resource(即你配的
monotonic_buffer_resource或new_delete_resource) - 务必用
upstream参数把它链到一个大块预分配资源上,否则它自己还是会调new
std::pmr::synchronized_pool_resource pool{
std::pmr::pool_options{.max_blocks_per_chunk = 32},
&monotonic_pool // ← upstream,必须提供
};
真正难的不是选哪个 resource,而是把 allocator 透传到每一层 tensor 创建、kernel launch、host-to-device copy 的入口点。很多抖动不是来自 malloc 本身,而是某处隐式 new 没被拦截住——得靠 ASan + malloc trace 双验证。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











