返回专辑
·Johan·5 分钟阅读

shared_ptr 成本:何时不该共享所有权

shared_ptr 的原子 refcount 成本与何时该 unique_ptr + 借用。

shared_ptr 成本:何时不该共享所有权

1. 默认 shared 不是省事,是语义选择

perception pipeline 里每个 cell 按值传 shared_ptr<PointCloud>,profiler 里 std::_Sp_counted_base::_M_release 占可观比例——CPU 不高,cache line 却在抖。同线程 DAG 里 pass-by-const-ref 足够;只有跨线程延长生命周期或多订阅者真共享才需要 atomic refcount。

shared_ptr 方便共享帧缓冲,但 atomic refcount 每次 copy 都有成本,且循环引用 leak。默认用 unique_ptr + move;只有多订阅者真共享才 shared_ptr<const Frame>。共享所有权是语义选择——不是默认智能指针。

2. 成本在哪

cpp
void pass(std::shared_ptr<Cloud> c);  // copy → atomic fetch_add
void pass(const Cloud& c);              // 同线程无 atomic
void to_worker(std::shared_ptr<const Cloud> c) {
  pool.submit([c = std::move(c)] { process(*c); });
}

hot loop 按值传 shared_ptr 是反模式——传 const Cloud&span 或 move 出唯一 owner。make_shared 一次分配 control+object,比 shared_ptr(new T) 少一次 malloc;但 control 与 object 同寿命会延长 dealloc——大数组场景要权衡。每 copy 一次 atomic inc/dec,DAG 里 pass-by-const-ref 只在跨线程延长生命周期时用 shared。

3. weak_ptr 与 enable_shared_from_this

viz 缓存 map<Id, shared_ptr<Mesh>> 若 mesh 持 shared_ptr<Renderer> 回指会 leak——缓存 value 改 weak_ptr,用时 lock。双向链表:shared_ptr next + weak_ptr prev,别双向 shared。perception 节点里 shared 环常来自 logger callback 捕获 node——用 weak_ptr 或 lifecycle inactive 门控 break 环。

enable_shared_from_this 仅在对象已被 shared_ptr 管理后可用;构造完成前 shared_from_this() UB。异步 post 构造回调用 weak 或分两阶段 init。构造完成前 shared_from_this() 是常见 startup crash 源。

4. 与 ROS loaned message

零 copy 借 message 时生命周期由 middleware 管——别 wrap 成 shared_ptr 误延长或 double-free。loan 语义是「借」,不是「共享所有权」。把 loaned buffer 包进 shared_ptr 自定义 deleter 是高级玩法,团队须统一 deleter 契约,别各节点各写一套。

5. 案例:pipeline 里的 refcount 热点

点云 pipeline 每 stage 按值传 shared_ptr,profiler 里 _M_add_ref 占 8%。热路径改成 driver 队列 unique_ptr move 出,下游只读时 shared_ptr<const Msg> 按 const& 传参,worker 仅 move capture 一次。帧时间变化不大,但 overnight RSS 曲线平了——refcount 争用被削掉。诊断:perf record -g -e cycles:u_M_add_ref;若高,画 ownership 图砍副本。说明 shared 的成本不总在 CPU%,而在 cache 与 tail latency。

6. 传感器 callback 的合理 shared

传感器 callback 里 shared_ptr<Frame> 进队列是合理场景——worker 须延长 frame lifetime。但同线程后续处理链不必每层 copy shared;move 出队列后下游 const ref 即可。viz 缓存 mesh 用 weak_ptr 升级,别 30Hz 环里循环 lock。

7. 控制块与定制 deleter

shared_ptr 自定义 deleter 适合 wrap 非 new 资源,但 deleter 类型进入 shared_ptr 类型,ABI 与回调链复杂化。传感器 DMA buffer 用 deleter 调 driver release 可以,须全团队统一,别每节点一个 deleter 实现。make_shared 控制块与对象同块,weak_ptr 多时延长 object 生命——大 payload 场景评估是否 split 分配。

9. 与 lifecycle 的交叉

node inactive 时 callback 仍可能持有 shared_ptr 延长 subgraph——lifecycle 转换前先 flush 队列、weak 掉 viz 缓存。shutdown hang 多半是环或 worker 仍 hold shared;lifecycle 测试里 repeated activate/deactivate 查 RSS 与 join 时间。logger 回调捕获 shared_ptr<Node> 是常见环——改 weak 或在 inactive unregister。

10. 所有权图与 code review

合入 hot path 新增 shared_ptr 按值传参前,画 ownership:谁 create、谁最后 release、跨几条线程 copy。若同一帧 copy 超过两次且无多订阅者,改 const ref 或 move。文档写清「此类型默认 unique,何时升级 shared」——比口头 tradition 可靠。

9. 验收

  • perf record -g_M_add_ref / _M_release;高则砍副本。
  • 无 shared 环导致 node shutdown hang。
  • loaned message 不误 wrap 成 shared。
  • ownership 图对 hot path 可解释。

12. 与消息队列、DDS 的交叉

DDS loan 与 shared_ptr 语义不同——loan 归还后 refcount 无关。把 loan wrap 成 shared 自定义 deleter 可以,但 deleter 必须调 middleware return API,且全 fleet 统一。传感器 driver 出队 unique_ptr move 给 perception,仅在 viz 或多订阅分支 upgrade shared,是常见清晰分层。诊断 shutdown hang 时先查 shared 环与 logger callback 捕获,再查线程 join——两者都常表现为「进程不退」。style guide 写一句:hot path 禁止按值传 shared_ptr,review bot 可 grep hot path 按值 shared_ptr 签名辅助——文化比工具重要,但工具能挡低级回归。ownership 图画在 design doc,比事后 perf 猜副本链省时间。

refcount 争用不占 CPU 百分比高,却拉高 tail latency——对 30Hz 闭环仍敏感。性能预算里给「每帧 shared copy 次数」设上限;超线 PR 须 justification。viz/rviz 缓存是 shared 合理区,decode hot path 不是。make_shared 与单独 new 的分配差异在高频 create/destroy 路径才明显,mid-pipeline 传参才是主要 atomic 税。

13. 与 unique_ptr 的迁移路径

legacy 代码全是 shared 时,先画 hot path ownership,再逐段改 unique move——别一次性全改引发 leak。中间态可在 queue 边界保留 shared,内核 DAG 改 const ref。迁移 PR 附 profiler 前后对比与 shutdown 测试,证明环已 break。共享所有权解决真问题——多线程队列、多订阅只读帧;默认 shared 是在用 atomic 税买不需要的语义。

← 全部文章

johan's blog