1. 引言:为什么需要对比两者?
在现代C++开发中,引用计数(Reference Counting)是一种常见的内存管理技术,用于自动管理对象的生命周期。C++标准库提供了std::shared_ptr
作为通用的引用计数智能指针,而Chromium项目则自定义了一套引用计数系统(base::RefCounted
和base::RefCountedThreadSafe
)。
为什么Chromium不直接使用std::shared_ptr
?
性能优化:Chromium对内存和CPU效率要求极高,
std::shared_ptr
的全局原子操作可能带来不必要的开销。线程模型控制:Chromium的线程模型(如UI线程、IO线程、GPU线程)需要更细粒度的线程安全控制。
内存占用优化:
std::shared_ptr
的控制块(Control Block)会增加额外内存开销,而Chromium的base::RefCounted
将引用计数直接嵌入对象,减少内存占用。
本文将深入对比base::RefCounted
(Chromium风格)和std::shared_ptr
(C++标准库)的设计差异,涵盖线程安全、性能优化、内存模型、功能扩展等方面,帮助开发者理解它们的适用场景。
2. 线程安全性:最关键的差异
2.1 base::RefCountedThreadSafe
(线程安全版本)
Chromium提供了两种引用计数模板:
base::RefCounted
(非线程安全,引用计数无原子保护)base::RefCountedThreadSafe
(线程安全,引用计数使用原子操作)
关键特点
仅保证引用计数的原子性
AddRef()
和Release()
使用std::atomic
保证线程安全。但对象的析构仍需开发者控制(通常需回到创建线程析构,避免竞态条件)。
适用于跨线程共享对象
例如网络模块、异步任务等需要多线程访问的场景。
示例代码
class NetworkResource : public base::RefCountedThreadSafe<NetworkResource> { public: void Fetch() { /* 跨线程操作 */ } }; // 跨线程传递 scoped_refptr<NetworkResource> resource = base::MakeRefCounted<NetworkResource>(); io_thread.PostTask(FROM_HERE, base::BindOnce(&NetworkResource::Fetch, resource));
2.2 std::shared_ptr
(全局线程安全)
std::shared_ptr
的设计目标是通用性,因此它的线程安全策略更加严格:
控制块(Control Block)完全线程安全
强引用计数、弱引用计数、删除器(Deleter)均使用原子操作。
对象析构可能发生在任意线程
最后一个
shared_ptr
释放时,对象析构可能不在创建线程(可能影响线程局部存储)。
示例代码
auto resource = std::make_shared<NetworkResource>(); std::thread([resource] { resource->Fetch(); // 可能在任何线程析构 }).detach();
2.3 对比总结
维度 | base::RefCountedThreadSafe |
std::shared_ptr |
---|---|---|
引用计数原子性 | 仅AddRef/Release 原子 |
控制块全原子(强/弱引用+删除器) |
析构线程安全性 | 需手动控制(如PostTask 回原线程) |
可在任意线程析构 |
适用场景 | 需要精细线程控制的系统(如浏览器) | 通用跨平台代码 |
关键结论:
如果需要在特定线程析构对象(如UI线程),
base::RefCountedThreadSafe
更合适。如果希望自动管理析构线程,
std::shared_ptr
更方便,但可能牺牲性能。
3. 性能优化:内存与原子操作开销
3.1 内存占用对比
实现方式 | base::RefCounted |
std::shared_ptr |
---|---|---|
存储位置 | 引用计数嵌入对象(无额外开销) | 额外控制块(多2个指针大小) |
内存占用 | 更小 | 更大(除非使用make_shared 优化) |
std::shared_ptr
的内存布局
+-------------------+ +-------------------+ | shared_ptr<T> | ---> | Control Block | | - 对象指针 | | - 强引用计数 | +-------------------+ | - 弱引用计数 | | - 删除器 | | - 对象内存(可选) | +-------------------+
如果使用
new T
+shared_ptr
,对象和控制块分开分配(2次内存分配)。如果使用
make_shared
,对象和控制块可能合并(1次分配)。
base::RefCounted
的内存布局
+-------------------+ | RefCounted<T> | | - ref_count_ | // 引用计数直接存储在对象内 | - 其他成员 | +-------------------+
无额外控制块,内存更紧凑。
3.2 原子操作开销
操作 | base::RefCounted (非线程安全) |
base::RefCountedThreadSafe |
std::shared_ptr |
---|---|---|---|
引用计数增减 | 普通整数操作(无锁) | 原子操作(std::atomic ) |
原子操作 |
适用场景 | 单线程(如UI线程) | 多线程共享 | 通用多线程 |
性能影响:
原子操作(如
fetch_add
)比普通操作慢约2-10倍(取决于CPU架构)。Chromium的
base::RefCounted
允许在单线程场景禁用原子操作,提升性能。
4. 功能扩展:弱引用与循环引用处理
4.1 std::shared_ptr
的weak_ptr
机制
std::weak_ptr
用于解决循环引用问题,例如:
struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // 避免循环引用 };
优点:自动检测对象是否存活,避免内存泄漏。
缺点:控制块需额外维护弱引用计数,增加内存开销。
4.2 Chromium的替代方案
Chromium未内置弱引用,但提供了base::WeakPtrFactory
:
class Document : public base::RefCounted<Document> { public: base::WeakPtr<Document> GetWeakPtr() { return weak_factory_.GetWeakPtr(); } private: base::WeakPtrFactory<Document> weak_factory_{this}; };
缺点:需手动管理,不如
weak_ptr
方便。优点:无额外控制块,内存更高效。
5. 设计哲学:为何Chromium选择自定义实现?
极致性能优化
浏览器对内存和CPU敏感,
std::shared_ptr
的通用性带来额外开销。
明确线程模型
Chromium的线程任务需精细控制(如UI线程仅允许特定操作)。
减少依赖
自定义实现避免受标准库实现的限制(如ABI兼容性问题)。
6. 最佳实践:如何选择?
场景 | 推荐方案 |
---|---|
高性能单线程对象 | base::RefCounted |
跨线程共享对象 | base::RefCountedThreadSafe |
通用代码,需弱引用支持 | std::shared_ptr + weak_ptr |
内存敏感型应用 | Chromium风格(无控制块开销) |
7. 总结
维度 | base::RefCounted |
std::shared_ptr |
---|---|---|
线程安全 | 可定制(安全/非安全) | 全局原子(可能性能损失) |
内存开销 | 更小(无控制块) | 更大(控制块+弱引用) |
适用场景 | 高性能、线程模型明确的系统 | 通用代码、需要弱引用 |
最终建议:
如果是Chromium开发或高性能中间件,优先使用
base::RefCounted
。如果是通用C++项目,
std::shared_ptr
更方便。
延伸阅读: