C++ std::shared_ptr 的引用计数开销是什么?(如何通过 atomic 实现并发安全)

1次阅读

std::shared_ptr的引用计数必须是原子的,因为线程下拷贝、赋值、析构会并发读写同一计数,非原子操作会导致丢失自增、提前释放对象(uaf);标准强制要求原子实现,即使单线程也无法绕过,代价是额外指令与缓存行访问。

C++ std::shared_ptr 的引用计数开销是什么?(如何通过 atomic 实现并发安全)

std::shared_ptr 的引用计数为什么必须是原子的?

因为 std::shared_ptr 的拷贝、赋值、析构都可能在多线程中并发发生,而这些操作都要读写同一个引用计数。如果计数不是原子的,两个线程同时执行 ++count 就可能丢失一次自增,导致提前释放对象——这是典型的 UAF(Use-After-Free)漏洞。

标准要求这个计数必须用原子操作实现,不管底层用的是 std::atomic<int></int> 还是平台级的 lock-free 指令(比如 x86 的 lock inc),目的只有一个:保证每次增减都不可分割。

  • 即使你只在单线程里用 std::shared_ptr,它的引用计数类型仍是原子的,无法绕过(除非手写定制删减版)
  • 原子操作本身有代价:在 ARM 等弱内存序平台上,fetch_add 可能隐含 full memory barrier;x86 虽然较轻,但仍有指令开销
  • 别指望编译器帮你“优化掉”原子性——它不会,也不能

引用计数开销具体有多大?

不是“慢”,而是“比裸指针多几条指令 + 一次缓存行访问”。典型场景下,一次 shared_ptr 拷贝包含:

  • 对控制块中引用计数执行一次原子 fetch_add(1)
  • 可能触发 cache line bouncing(多个线程频繁修改同一 cache line 中的计数)
  • 析构时还要再做一次原子 fetch_sub(1),并检查是否为 0 决定是否 delete 对象和控制块

实测:在主流 x86-64 上,一次 shared_ptr 拷贝比 unique_ptr 拷贝慢 2–5 倍(取决于缓存状态),但绝对耗时仍在纳秒级(~1–3 ns)。真正伤性能的是高频共享+频繁拷贝,比如在 tight loop 里反复传参或存入容器。

立即学习C++免费学习笔记(深入)”;

什么时候可以避免引用计数开销?

如果你确定某个对象生命周期完全由单一线程管理,且不需要跨函数/模块共享所有权,就别用 std::shared_ptr

  • 优先用 std::unique_ptr:移动语义零开销,无原子操作
  • 若只是临时观察,用裸指针或 std::observer_ptrc++17,纯语法糖,不干预生命周期)
  • 若需跨线程传递所有权,考虑 std::move 一个 unique_ptr,而不是拷贝 shared_ptr
  • 注意:把 shared_ptr 存进 std::vector 并反复 push_back,等于反复触发原子增减——这里最容易被忽略

自定义删除器或别名构造会加重开销吗?

不会额外增加引用计数本身的开销,但会影响控制块布局和缓存效率。

  • 带自定义删除器的 shared_ptr 必须把删除器对象存进控制块,增大控制块尺寸,可能让原本紧凑的 cache line 更容易 miss
  • 别名构造(如 shared_ptr<t>(p, alias)</t>)会复用原控制块,不新增计数操作,但要注意:它延长了原对象的生命周期,而你可能没意识到
  • 所有这些都不改变引用计数的原子性逻辑——该原子还是原子,该慢还是慢

真正难察觉的是:控制块分配本身是上的,而堆分配+原子操作+缓存竞争三者叠加,在高并发场景下会让延迟毛刺明显起来。这不是 bug,是设计权衡的结果。

text=ZqhQzanResources