摘要 :嵌入式工程师往往因为惧怕内存碎片而拒绝使用 STL 容器。传统的自定义 Allocator 写法极其繁琐且类型不兼容。本文将介绍 C++17 的 多态内存资源 (PMR) ,演示如何利用
monotonic_buffer_resource实现 "栈上变长数组" ,让std::vector的数据完全存储在局部栈变量中,实现零堆内存分配 和确定性性能。
一、 痛点:被妖魔化的 STL
你是否想过在嵌入式项目里用 std::vector 来存储一串不确定长度的传感器数据?
传统的 vector 隐患
void ProcessSensorData() {
// 默认分配器:调用 malloc/new
std::vector<int> data;
// 隐患 1:如果数据很多,触发多次 realloc,导致内存搬运,耗时不可控。
// 隐患 2:函数结束 vector 销毁,留下一堆小的内存碎片。
// 隐患 3:如果堆满了,系统抛异常或死机。
for (int i = 0; i < 100; i++) {
data.push_back(ReadADC());
}
}
为了安全,大多数嵌入式规范(如 MISRA C++)直接禁止使用动态内存。于是我们只能回退到写死的 int data[100],既浪费空间又怕越界。
二、 救星:std::pmr (C++17)
C++17 引入了 <memory_resource> 头文件。它把 "容器的逻辑" 和 "内存从哪来" 彻底分开了。
我们不再需要写复杂的 template <typename T> struct MyAllocator。我们只需要给容器传一个指针:std::pmr::memory_resource*。
核心组件:
-
std::pmr::vector<T>:这是std::vector<T, std::pmr::polymorphic_allocator<T>>的别名。它和普通 vector 用法一样,但支持运行时切换内存源。 -
std::pmr::monotonic_buffer_resource:这是神器。它只管分配,不管释放(直到析构时统一释放)。分配速度就是指针加法,极快。
三、 实战:在栈上创建 vector
我们要实现的效果:创建一个 vector,但它的内存全部占用当前的 栈空间 (Stack),不需要去堆上申请。
代码实现
#include <vector>
#include <memory_resource>
#include <array>
#include <cstdio>
void StackVectorDemo() {
// 1. 准备一块栈内存 (比如 1KB)
// 这只是一个普通的字节数组,完全在栈上
std::array<std::byte, 1024> buffer;
// 2. 创建一个"单调缓冲区资源"
// 告诉它:用 buffer 这块地,如果 buffer 用完了,再去问上游要 (nullptr 表示用完就崩/报错)
std::pmr::monotonic_buffer_resource pool(
buffer.data(),
buffer.size(),
std::pmr::null_memory_resource()
);
// 3. 创建 vector,传入 resource 指针
// 注意:这个 vector 的数据不走 malloc,全在 buffer 里!
std::pmr::vector<int> vec(&pool);
// 4. 正常使用
for (int i = 0; i < 50; i++) {
vec.push_back(i);
// 这里的内存分配,本质上只是 pool 内部的 current_ptr += sizeof(int)
// 速度等同于直接写数组
}
printf("Vector size: %d\n", vec.size());
// 5. 函数结束
// vec 析构 -> pool 析构 -> buffer 随栈自动回收
// 全程没有一次 malloc/free 调用!
}
深度解析:发生了什么?
-
初始化 :
pool记录了buffer的首地址。 -
push_back :
vec发现容量不够,调用pool->allocate()。 -
allocate :
pool检查buffer还有空位,直接返回当前指针,并将指针后移。O(1) 复杂度。 -
销毁 :
vec析构时调用pool->deallocate()。 -
deallocate :
monotonic_buffer什么都不做 (no-op)。因为它是单调的,只能线性增长。所有的内存回收依赖于buffer本身离开作用域。
这相当于实现了一个**"具有 vector 接口的变长数组 (VLA)"**,但比 C 语言的 VLA 安全得多(有越界检查、容量检查)。
四、 进阶:解决碎片化的 pool_resource
上面的 monotonic 适合"用完即扔"的临时场景。如果你需要长期运行的任务,且对象大小固定(比如网络包处理),可以使用 unsynchronized_pool_resource。
它通过维护一系列 固定大小的内存块 (Block Pools) 来解决碎片问题。
// 全局的内存池,放在 BSS 段
static std::array<std::byte, 1024 * 10> g_GlobalBuffer;
// 线程不安全的池资源(嵌入式单任务通常不需要锁,为了速度)
std::pmr::unsynchronized_pool_resource g_Pool(
std::pmr::pool_options{16, 1024}, // 最小块 16B,最大块 1024B
new std::pmr::monotonic_buffer_resource( // 上游资源
g_GlobalBuffer.data(),
g_GlobalBuffer.size()
)
);
void Task() {
// 使用全局池
std::pmr::vector<int> packet(&g_Pool);
packet.resize(20);
// ... 处理 ...
// packet 析构时,内存归还给 g_Pool,
// g_Pool 会把这块内存标记为"空闲",供下次重复利用
// 依然没有 malloc/free,彻底杜绝碎片。
}
五、 性能与开销分析
问:这东西有虚函数,会慢吗?
答:
-
调用开销 :
std::pmr::vector调用allocate时确实是一次虚函数调用 (do_allocate)。这是一个间接跳转。 -
分配开销:
-
普通 malloc:涉及复杂的空闲链表搜索、合并、锁竞争。耗时几十到几百个周期。
-
Monotonic :仅仅是
ptr += bytes。耗时 1-3 个周期。
-
-
总账:虚函数的那点开销(几纳秒),相比于 malloc 省下的时间(几微秒),简直可以忽略不计。
结论 :std::pmr::vector 在配合 monotonic_buffer 使用时,性能远超 标准 std::vector。
六、 嵌入式适配指南
要在 STM32/ESP32 上使用 PMR,需要注意:
-
编译器版本 :必须开启 C++17 (
-std=c++17)。GCC 9+ 或 Clang 10+ 支持较好。 -
异常处理 :如果在栈上 buffer 用完了,PMR 默认会抛
std::bad_alloc。在禁用了异常的嵌入式环境中,这通常会导致abort()。- 建议 :给 buffer 留足余量,或者通过
pool_options限制最大分配。
- 建议 :给 buffer 留足余量,或者通过
-
库支持 :某些精简版 C++ 运行时(如 newlib-nano)可能默认没链接 PMR 实现。如果没有,你需要自己继承
std::pmr::memory_resource实现一个简单的基类(非常简单,只有 3 个虚函数)。
七、 总结
std::pmr 是 Modern C++ 送给嵌入式开发者的重磅礼物。它打破了"嵌入式不能用 STL"的魔咒。
-
栈上 Vector :利用
monotonic_buffer,把 vector 当作安全的 VLA 用。 -
零碎片 :利用
pool_resource,在静态内存中高效复用内存块。 -
代码复用 :你的算法代码可以统一接收
std::pmr::vector&,无论内存是来自栈、静态区还是外部 SDRAM,算法都不需要修改。
别再手写链表和动态数组了,用 PMR 拥抱现代化的内存管理吧。