为何Rust中Arc与Rc的性能差异随引用向量大小变化?
Arc vs Rc 多线程读取场景的性能差异分析
在完成智能指针练习时,我好奇多线程读取场景下,引用数字列表的Arc性能开销如何,于是编写了测试程序:单线程用Rc对向量求和10次取平均耗时,再用10个并行线程通过Arc求和10次,对比两者耗时。
测试代码
use std::sync::Arc; use std::rc::Rc; use std::thread; use std::time::Instant; fn main() { let numbers: Vec<_> = (0..1_000_000_u128).collect(); let shared_arc = Arc::new(numbers.clone()); let shared_rc = Rc::new(numbers); let mut joinhandles = Vec::new(); // Test Rc let start = Instant::now(); for _ in 0..10 { (*shared_rc).iter().sum::<u128>(); } let t1 = start.elapsed(); println!("{:?}", t1 / 10); // Test Arc let t2 = Instant::now(); for _ in 0..10 { let thread_numbers = Arc::clone(&shared_arc); joinhandles.push(thread::spawn(move || { (*thread_numbers).iter().sum::<u128>(); })) } // Join all threads for handle in joinhandles { handle.join().unwrap(); } let t3 = t2.elapsed(); println!("{:?}", t3); }
测试结果
Size numbers | Time Rc | Time Arc |
|---|---|---|
| 1 000 000 | 4.7 ms | 8.5 ms |
| 10 000 000 | 47 ms | 99 ms |
| 100 000 000 | 497 ms | 1088 ms |
可以看到,使用Arc的总耗时通常是Rc单线程平均耗时的两倍!我原本以为Arc与Rc仅在引用计数更新的线程安全性上有区别,性能开销仅与Arc::clone的调用次数相关,而非引用向量的大小。猜测原因可能是多线程特性导致的性能损耗,或是(A)Rc对象在每次线程解引用时被修改,想知道具体是哪一种原因?
补充说明:测试未开启编译器优化,CPU为i7 8700K(6核12线程,12MB缓存)
问题解析
场景不对等是核心原因
- Rc测试是单线程串行执行10次求和:第一次加载向量到CPU缓存后,后续9次求和都直接从缓存读取,缓存命中率极高,所以平均耗时低。
- Arc测试是10个线程并行求和:多个CPU核心同时读取同一大向量,触发**缓存一致性协议(如MESI)**的同步开销。当一个核心修改缓存行状态(哪怕是只读操作导致的共享状态同步),其他核心需要等待缓存行同步完成才能读取,向量越大,需要同步的缓存行越多,耗时增长越明显,这才是耗时翻倍的关键。
Arc的clone开销可忽略
Arc的clone只是原子递增引用计数,这个操作是纳秒级的,和向量大小完全无关,不会导致如此大的耗时差异。解引用不会修改引用计数
无论是Rc还是Arc,只有在clone(增加引用)和drop(减少引用)时才会操作引用计数。求和过程只是读取向量内容,完全不会修改引用计数,所以这部分没有额外开销。未开优化放大了差异
Debug模式下Rust生成的代码包含大量调试逻辑,单线程的缓存优势被进一步放大,而多线程的缓存竞争开销也更突出。开启--release优化后,单线程耗时会降低,多线程的缓存竞争占比会更真实,但核心的缓存同步问题依然存在。
内容的提问来源于stack exchange,提问作者Encephala
相关产品推荐
相关产品推荐

