
1. 延迟尖刺往往来自「看不见的 malloc」
Nav2 costmap 更新回调每帧触发,用 perf record 看用户态热点,_Znwm 占比不低,但源码里找不到显式 new。根因常在 Small Buffer Optimization(SBO)失效:std::function 捕获略大的 lambda 会 spill 到堆;中等长度 fmt::format 返回的 string 越过 SSO 阈值;临时邻域索引用 std::vector 每次 push_back 都可能触发 realloc。机器人 hot path 里,短 topic 名、小 polygon 顶点、几十元素的 inlier 列表——本可在栈上完成,却反复进出堆。
SBO 不是银弹,但你要能判断:当前类型在典型负载下是否仍走栈内缓冲,失效时是否有可替代的 owning 策略。否则 p99 latency 会先报警,而 mean 仍「看起来正常」。
2. std::string 的 SSO 与边界
libstdc++/libc++ 的 SSO 阈值实现相关(常见约 15–22 字节)。frame_id = "lidar_link" 往往零 heap;日志 tag、短关节名、固定前缀 error_code 是受益者。别用 std::string(1024, 'x') 当格式化 buffer——那是刻意的堆分配,和 SSO 无关。
中等长度字符串最尴尬:刚过 SSO 又不够大,reserve 也救不了首次 alloc。热路径可改用 fmt::memory_buffer 或 std::array<char, N> 格式化后包 string_view 传给 log sink,避免每帧构造 owning string。
std::string frame_id{"cam0"}; // 常见实现下 SSO
// 热路径避免每帧:
// auto s = fmt::format("seq={} stamp={}", seq, t);3. SmallVector 与 std::function
llvm::SmallVector<T, N> 把前 N 个元素放栈上,超出才 spill——适合「大多数 scan 几十 inlier、极少数上千」这类分布。RANSAC 内点收集、costmap 临时索引、局部邻域搜索是典型场景。选 N 时看 profiler 上元素个数分布的 knee,不是拍脑袋 256。
llvm::SmallVector<Eigen::Vector3f, 256> inliers;
inliers.reserve(expected);std::function 内置小 buffer(libstdc++ 常见约 32B);capture 含 shared_ptr 或大对象时几乎必 heap。costmap 回调里改固定函数指针或 fu2::function 后,overnight 长跑有时能观察到 VmRSS 不再线性爬升——先 profile 再改,别盲换库。
4. 何时别用、失效信号
元素大或 N 过大:SmallVector 嵌在大 struct 数组里会撑爆栈帧,嵌入式栈默认几 MB,别假设桌面栈深度。生命周期跨 async:栈缓冲不能随 callback 返回而失效;异步仍要 owning storage 或 copy。spill 到 heap 后 iterator 与 data() 指针可能失效——filter 里 extend 后别缓存裸指针跨 push_back。1M 点云级别数据禁止 stack SmallVector;profiler 确认 operator new 热点后再引入 SBO。
验证:perf stat -e page-faults ./hot_loop 对比改前改后 minor faults;perf record -g 搜 _Znwm 占比。
5. 与 fmt、Pimpl 的联动
Pimpl 里短 string 同样受益于 SSO。review 时看到热路径每帧 fmt::format 进 string,先问能否 thread_local buffer 或 memory_buffer 一次性 write log sink。lambda 捕获大 shared_ptr 进 function 是常见 SBO 失效源——能否改成 string_view/span 引用,由外部保证 lifetime。
std::array<char, 64> 作 stack format buffer 再 string_view 传 log,适合中等长度、反复格式化的 tag。reserve 在已知 upper bound 时避免 realloc 抖动,但对 SSO 边界无效。
6. 案例:回调里的 hidden alloc
某 perception 节点把 costmap 更新包进 std::function,capture 两个 shared_ptr 加一个 vector<int>——每帧 malloc。改成成员函数指针加预分配 scratch buffer 后,perf stat -e page-faults minor faults 降一个数量级,p99 latency 从 8ms 回到 3ms。不是算法变快,是 stop 做无用堆 trip。这类问题在集成测试里很难看见,须专门对 hot callback 做 alloc profiling。
7. 与 std::vector 短序列的对比
小 N 时 vector 仍 heap;短序列考虑 std::array<Vector3f, 64> 或 SmallVector。Eigen aligned 容器小 N 也 heap——别默认 vector<Eigen::Vector3f, aligned_allocator> 会走 SBO。选容器前先画 alloc 链:谁分配、谁释放、是否每帧。
8. 与录包、集成测试的关系
alloc 问题在单元测试里往往看不见——mock 数据太小,始终走 SSO。集成测试应跑真实 bag 长度 topic 名、真实 costmap 顶点数,并对 hot callback 开 perf stat。录包复盘时若 p99 尖刺与帧率无关,优先查 hidden alloc 而非先调算法参数。团队 review checklist 加一条:热路径新增 std::function、返回 std::string、vector push 须有 profiler 依据或改 SBO 方案。
9. 验收
- hot loop 中
operator new调用次数不与帧率线性绑定。 - SSO 边界 case(14/16/32 字节 tag)无意外 heap。
- SmallVector spill 路径有 stress case,确认无 UAF。
perf record -g搜_Znwm;占比高则画 alloc 链再改。- 1M 点云勿 stack 分配;栈溢出比 malloc 更致命。
SBO 是微优化,但在 1MHz 级调用或 30Hz 全链路里,堆分配从「偶尔」变「每帧」,latency 尾迹会先于 mean 报警。先看 operator new 热点,再决定 SmallVector——别把百万点塞进栈。
相关
也可以看看
johan's blog