C++标准库容器默认使用std::allocator,它在编译期就固定了分配策略,实际执行时基本等价于直接调用全局的operator new和operator delete。当程序里存在大量短生命周期的小对象、频繁创建销毁容器时,这种默认行为会带来明显的性能开销和内存碎片。C++17引入的std::pmr(polymorphic memory resources,多态内存资源)正是为了解决这个问题:它把分配策略从类型系统中抽离出来,放到运行时,通过一个继承自std::pmr::memory_resource的抽象基类来决定内存从哪里来、回哪里去。最关键的一点是,多个容器可以绑定到同一个资源对象上,实现真正意义上的内存池复用。

一、memory_resource与polymorphic_allocator的运行时分发原理
整个std::pmr体系的核心是std::pmr::memory_resource这个抽象基类,它定义了三个纯虚函数:do_allocate、do_deallocate和do_is_equal。任何自定义的分配策略,本质上就是继承这个类并实现这三个虚函数。而std::pmr::polymorphic_allocator<T>内部只持有一个指向memory_resource的裸指针,当容器调用allocator的allocate时,allocator会把请求转发给这个指针指向的资源对象,由虚函数表决定实际执行哪段分配逻辑。
这个设计带来一个和传统allocator完全不同的特性:allocator的类型是同一个(polymorphic_allocator<int>),但行为可以随绑定的资源不同而不同。容器拷贝、移动时,pmr容器会通过select_on_container_copy_construction保证新容器继承构造时显式传入的资源,而不是连源容器的资源一起拷过来,这避免了资源对象生命周期管理的混乱。需要注意的是,资源对象的生命周期必须长于所有引用它的容器,否则释放内存时会触发未定义行为,这是使用pmr时最容易踩的坑。
标准库在std::pmr命名空间下提供了几个预置的pmr容器别名,例如std::pmr::vector<int>实际上就是std::vector<int, std::pmr::polymorphic_allocator<int>>。使用时务必用这些别名,不要手写模板参数,可读性会好很多。
二、用pool_resource让多个容器共享内存池
std::pmr提供的三种标准资源各有分工:new_delete_resource是默认行为;monotonic_buffer_resource只分配不回收,析构时一次性归还,适合一次性批量构建场景;synchronized_pool_resource和unsynchronized_pool_resource则是真正的池化资源,内部按块大小分桶管理,小块从池里取,大块回退到上游资源,且回收的内存可以被后续分配直接复用。
下面的例子展示了如何让多个不同类型的容器共享同一个内存池:
#include <memory_resource>
#include <vector>
#include <map>
#include <string>
#include <cstdio>
int main() {
// 上游资源:池子不够用时最终从这里拿内存
std::pmr::memory_resource* upstream = std::pmr::new_delete_resource();
// 创建一个线程安全的内存池
std::pmr::synchronized_pool_resource pool{
std::pmr::pool_options{.max_blocks_per_chunk = 512,
.largest_required_pool_block = 4096},
upstream};
// 三个容器全部绑定到同一个pool上
std::pmr::vector<int> nums{&pool};
std::pmr::vector<std::pmr::string> words{&pool};
std::pmr::map<int, double, std::less<int>,
std::pmr::polymorphic_allocator<std::pair<const int, double>>> table{&pool};
for (int i = 0; i < 1000; ++i) {
nums.push_back(i);
words.emplace_back("item_" + std::to_string(i));
table.emplace(i, i * 1.5);
}
// 此时所有内存都来自pool,pool析构时统一回收
std::printf("map size: %zu\n", table.size());
return 0;
}这段代码的关键在于每个容器构造时都传入了&pool。当nums扩容释放旧缓冲区时,那块内存回到池中,可能立刻被words的分配请求复用,这就是跨容器复用的本质。单线程场景下建议用unsynchronized_pool_resource,省掉每次分配的锁开销,性能差距在高频分配时相当可观。
另外要留意std::pmr::string,它是std::basic_string<char, char_traits<char>, polymorphic_allocator<char>>的别名。放在pmr容器里的字符串元素也应该用pmr版本,否则字符串内部仍会走全局分配,池化效果打折扣。同时,pmr string和普通std::string不能直接互相赋值,需要构造转换,接口设计时要提前规划好边界。
三、用monotonic_buffer_resource搭配栈上缓冲区做零开销分配
除了池化资源,monotonic_buffer_resource在特定场景下更加高效。它像一个只前进的指针:分配时指针往后挪,释放时什么都不做,析构时才整体归还。如果再给它配一块栈上的缓冲区,小规模计算完全可以做到零次堆分配:
#include <memory_resource>
#include <vector>
int main() {
// 栈上预分配64KB缓冲区,热数据优先从这里取
char buffer[64 * 1024];
std::pmr::monotonic_buffer_resource mono{
buffer, sizeof(buffer),
std::pmr::new_delete_resource()}; // 溢出时回退到堆
std::pmr::vector<int> a{&mono};
std::pmr::vector<double> b{&mono};
for (int i = 0; i < 2000; ++i) {
a.push_back(i);
b.push_back(i * 0.5);
}
// 作用域结束,mono析构,栈缓冲区自动"归还",全程可能一次堆分配都没有
return 0;
}一个实用技巧是把monotonic_buffer_resource作为pool_resource的上游资源,形成两级结构:池子负责小块的复用,monotonic负责大块的高速批发,这样既能复用又保留了极低的单次分配成本。很多游戏引擎和网络框架的帧级分配器就是这个套路——每帧开始创建一个monotonic资源,帧内所有临时容器都挂在它上面,帧结束时整体丢弃,碎片率趋近于零。
四、性能考量与常见误区
第一,pool资源不是万能药。如果容器的单次分配块很大(比如vector一次性扩容几百KB),会绕过池子直接走上游,池化收益归零,反而多了一层虚函数开销。可以通过pool_options的largest_required_pool_block评估业务里主流的分配尺寸,合理设置分桶边界。
第二,跨线程使用必须选synchronized_pool_resource,或者每个线程一个独立的unsynchronized_pool_resource。后者的吞吐通常更高,但要注意池与池之间内存不互通,总内存占用可能上升,需要结合实际并发度权衡。
第三,容器与资源的生命周期要严格保证资源后析构。推荐的做法是把资源对象声明在容器之前,或者用std::unique_ptr集中管理资源,让所有容器在模块退出前先销毁。一旦顺序颠倒,容器析构时通过悬垂指针调用资源的deallocate,程序会直接崩溃且难以定位。
综合来看,std::pmr的价值在于把内存策略从容器类型中解耦,一份池化代码可以服务vector、map、string等所有容器,配合两级资源嵌套,能在高频分配场景下把分配开销压缩一个数量级。如果你的模块里有大量生命周期相近的动态容器,值得认真评估这套方案。
C++多态分配器std::pmrmemory_resource修改时间:2026-09-03 13:15:00