为何增加线程无法进一步提升C++多线程程序性能?
关于std::atomic多线程计数的性能疑惑
我在学习C++多线程编程时,编写了一个简单测试程序:通过多个线程对std::atomic<int>类型的atomicCounter从0计数到10000000。测试发现,将线程数从8调整至16后,执行时间未大幅下降反而略有增加(8线程平均1.7ms,16线程平均1.9ms)。测试环境为搭载i9-12900K处理器(16核、支持24超线程)的机器。
原始测试代码
std::atomic<int> atomicCounter{0}; void addcount(int threadId) { while(atomicCounter.load() < 10000000) { atomicCounter.fetch_add(1); } } void test() { const int maxNumThreads = 8; // Time::now() is to get the timestamp accurate to ns auto s_ts = Time::now(); std::vector<std::thread> threads; for (int i = 0; i < maxNumThreads; i++) { threads.emplace_back([&]() { addcount(i); }); } // join the threads for (auto& thread : threads) { thread.join(); } threads.clear(); auto e_ts = Time::now(); LOG(INFO) << "Executing time : " << (e_ts - s_ts) / 1000 << " us"; }
之后我修改了代码(增加循环模拟任务获取与执行),调整线程数后性能符合预期(执行时间从3.0ms降至1.9ms),但仍不解原案例中性能不升反降的原因,恳请给出建议。
修改后的代码
std::atomic<int> atomicCounter{0}; void addcount(int threadId) { while(atomicCounter.load() < 1000) { //simulate threads fetch jobs from a queue and execute them once a time atomicCounter.fetch_add(1); for(int i = 0; i < 10000000; i++); } } void test() { const int maxNumThreads = 8; // get the timestamp accurate to ns auto s_ts = Time::now(); std::vector<std::thread> threads; for (int i = 0; i < maxNumThreads; i++) { threads.emplace_back([&]() { addcount(i); }); } // Join the threads for (auto& thread : threads) { thread.join(); } threads.clear(); auto e_ts = Time::now(); LOG(INFO) << "Executing time : " << (e_ts - s_ts) / 1000 << " us"; }
核心原因分析
原代码的性能瓶颈完全集中在原子操作的缓存竞争上:
std::atomic<int>的fetch_add是原子写操作,需要CPU间的缓存同步。当多个线程同时修改同一个原子变量时,会触发频繁的缓存失效——每个线程修改变量后,其他核心的对应缓存行会被标记为无效,必须重新从共享缓存或内存中读取最新值,这个过程的开销极大。- 8线程时,线程数处于物理核范围内(i9-12900K包含8个性能核+8个能效核),缓存竞争的开销尚在可控范围;当线程数增加到16时,不仅占用了所有物理核,还可能调度到超线程逻辑核。超线程共享同一物理核的执行资源和缓存,此时对同一个原子变量的竞争会进一步加剧,缓存同步的开销增长超过了线程数增加带来的并行收益,最终导致总执行时间上升。
而修改后的代码中,每个线程执行一次原子操作后会运行长循环模拟实际任务:
- 原子操作的频率大幅降低,缓存竞争的次数锐减,线程间的干扰显著减小。
- 长循环的计算任务可以充分利用多核心/超线程的并行能力,此时增加线程数带来的并行收益远大于缓存竞争的开销,因此性能符合预期。
优化建议
- 减少原子操作频率:避免多个线程频繁修改同一个原子变量,可采用线程本地计数+最终合并的方案——每个线程先在本地计数器累加,最后再将本地值合并到全局原子变量,大幅降低缓存竞争。
- 合理设置线程数:超线程更适合IO密集型或存在大量等待的任务;对于纯计算+高缓存竞争的场景,超线程会因资源共享导致性能下降,此时线程数设置为物理核数通常是最优选择。
内容的提问来源于stack exchange,提问作者Stars Lee
相关产品推荐
相关产品推荐

