为何C++并发代码test_1比test_2慢15倍?如何重构test_1支持可变线程数并达到同等性能
性能差距的根本原因
- 首先是未定义行为导致的悬垂引用问题:
test_1中创建的AlignedType a{};是for循环块内的局部栈变量,生命周期仅在单次循环的大括号作用域内有效。当emplace_back创建线程、把a.val的引用传递给线程函数后,作用域立即结束,a随即被栈回收,所有线程拿到的都是指向已销毁内存的非法引用。 - 其次是触发了极致的性能退化:根据栈内存的分配特性,每轮循环的局部变量
a都会落在栈上的同一个固定地址,4个线程实际操作的是完全同一块内存上的原子变量。哪怕AlignedType设计了64字节的缓存行对齐,也因为对象提前销毁、没有独立的对齐内存分配,所有线程的原子写操作都会反复触发跨核缓存一致性同步、缓存行失效,最终性能退化为单核心串行做原子操作的水平。 - 这种场景下的运行结果完全是内存布局的偶然产物,属于典型的未定义行为:可能出现程序崩溃、结果数值错误,本例中因为栈内存没有被后续逻辑覆盖,才表现为可观测的15倍性能差距。
支持可变线程数的高性能test_1重构
重构的核心目标是彻底消除悬垂引用、保证每个线程的计数器独立落在专属缓存行上,同时支持任意数量的线程启动,实现和test_2完全一致的运行效率:
void test_1(size_t thread_count = 4) { std::vector<std::thread> threads; threads.reserve(thread_count); // 提前批量分配与线程数匹配的计数器,每个元素因alignas(64)天然独占一个缓存行 std::vector<AlignedType> counters(thread_count); for (size_t i = 0; i < thread_count; i++) { // 传递独立计数器的引用,计数器生命周期覆盖线程全运行周期 threads.emplace_back(work, std::ref(counters[i].val)); } for (auto& thread : threads) { thread.join(); } }
重构后的实现特性:
- 所有计数器的生命周期与函数作用域绑定,从根源消除了悬垂引用的未定义行为
- 借助
AlignedType自带的64字节缓存行对齐,每个线程的计数器独占独立缓存行,完全避免伪共享带来的性能损耗 - 函数参数支持传入任意线程数量,满足可变线程数的设计要求,运行效率和手动创建独立变量的
test_2完全持平
内容的提问来源于stack exchange,提问作者topkek
相关产品推荐
相关产品推荐

