c++中如何避免std::shared_ptr的性能开销? (原子引用计数)

14次阅读

std::shared_ptr原子引用计数拖慢性能是因为即使单线程下也无法省略原子指令,导致内存屏障、优化受限及缓存一致性开销;实测tight loop中性能比裸指针低3–5倍。

c++中如何避免std::shared_ptr的性能开销? (原子引用计数)

为什么 std::shared_ptr 的原子引用计数会拖慢性能?

std::shared_ptr 默认使用原子操作(如 std::atomic_fetch_add)管理引用计数,确保多线程环境下安全。但即使在单线程场景下,编译器也无法省略这些原子指令——它们会强制内存屏障、禁用部分优化,并可能触发锁总线或缓存一致性协议开销。实测中,在 tight loop 里频繁拷贝/析构 std::shared_ptr,性能可能比裸指针低 3–5 倍。

如何关闭原子性:用 std::make_shared 配合自定义删除器?

不能直接“关掉”原子性——std::shared_ptr 标准规定其控制块的引用计数必须是线程安全的。但你可以绕过默认控制块,用 std::shared_ptr 的别名构造 + 自定义删除器来避免原子操作:

struct NonAtomicDeleter {     void operator()(int* p) const noexcept {         delete p;     } }; 

// 构造时不经过标准控制块,引用计数不启用原子操作 auto ptr = std::shared_ptr(new int(42), NonAtomicDeleter{});

注意:std::make_shared 无法用于此场景,因为它总会分配带原子计数的控制块;必须用原始指针 + 自定义删除器构造。

  • 该方式仅适用于明确单线程上下文,且你完全掌控所有 shared_ptr 实例的生命周期
  • 不能与通过 std::make_shared 创建的同对象 shared_ptr 混用,否则控制块不一致,导致未定义行为
  • 拷贝该 shared_ptr 仍会调用原子操作——因为 shared_ptr 的拷贝赋值/构造函数内部仍会访问控制块的引用计数

更实际的替代方案:std::unique_ptr 或裸指针 + 明确所有权

若不需要共享语义,std::shared_ptr 本身就是误用。性能问题只是表象,根源在于设计阶段没厘清所有权:

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

  • std::unique_ptr 替代:零运行时开销,移动语义清晰,且能配合 std::movestd::make_unique
  • 若需跨函数传递只读访问,优先传 const T&T*,而非 std::shared_ptr
  • 若确实要共享,但只在单线程内传播(如配置树、AST 节点),可封装一个非原子版 shared_ptr(如 Boost 的 boost::local_shared_ptr),它用普通整型计数,不依赖原子指令

何时必须接受原子开销?

只要出现以下任一情况,就无法规避原子引用计数:

  • 多个线程同时拷贝、赋值或销毁指向同一对象的 std::shared_ptr
  • 使用 std::weak_ptr —— 它依赖同一控制块中的原子弱引用计数
  • 通过 std::shared_ptr::owner_before 或比较操作做排序,因为实现依赖控制块地址和原子状态
  • std::shared_ptr 存入 std::vector 并频繁 resize —— 每次元素移动都触发引用计数增减

真正难处理的不是原子操作本身,而是人们把 std::shared_ptr 当作“不用动脑的所有权解决方案”,结果在 hot path 上反复构造/拷贝,却没意识到大部分时候只需要移动或引用。

text=ZqhQzanResources