多线程大内存写入耗时远超单线程的原因及优化咨询
1. rand()全局锁是性能雪崩的核心
gcc实现的rand()内部带全局互斥锁,10个线程同时调用时,所有线程都会卡在锁上,相当于从并行执行直接退化成串行,还额外增加了锁的上下文切换开销——这就是耗时从1秒跳到77秒的最主要原因。
2. 内存带宽饱和雪上加霜
单线程写0.8GB,10线程总写入量达到8GB。DDR4 3600的理论峰值带宽约28.8GB/s,但实际有效带宽(考虑内存控制器、通道利用率)大概在20GB/s上下。多线程同时写入会占满内存带宽,每个线程的写入速度被分摊,但这部分的性能下降远不如锁竞争严重,只是放大了问题。
3. Frontend停顿的原因
单线程时backend停顿占30%,主要是CPU等待内存写入完成的周期。多线程时frontend停顿占39%,是因为锁竞争导致线程频繁阻塞、上下文切换,CPU的指令取指单元没法持续获取连续的指令流;再加上rand()内部的分支逻辑容易触发分支预测失败,进一步加剧了frontend的停顿。
1. 换掉rand(),用线程安全的随机数
直接用C++11的<random>库,每个线程独立维护随机数引擎,彻底避免全局锁:
#include <random> #include <climits> void runLargeMemoryAlloOutside(benchmark::State & s) { const long size = 200'000'000; alignas(64) int* arr = new int[size]; // 每个线程独立的随机数生成器,thread_local保证线程独有 thread_local std::mt19937 rng(std::random_device{}()); thread_local std::uniform_int_distribution<int> dist(1, INT_MAX); // 避免log(0)报错 for (auto _ : s) { for (long i = 0; i < size; ++i) { const int tmp = dist(rng); arr[i] = static_cast<int>(log(tmp)); } } delete[] arr; }
2. 用AVX2向量指令提升内存写入效率
Ryzen 3900XT支持AVX2,一次能处理8个int,直接批量计算+写入,省去栈上tmp_arr的多余拷贝:
#include <immintrin.h> #include <random> #include <climits> #include <cmath> void runLargeMemoryAlloOutside(benchmark::State & s) { const long size = 200'000'000; alignas(64) int* arr = new int[size]; thread_local std::mt19937 rng(std::random_device{}()); thread_local std::uniform_int_distribution<int> dist(1, INT_MAX); const long vec_batch = size / 8; for (auto _ : s) { long i = 0; // 批量向量处理 for (; i < vec_batch * 8; i += 8) { int vals[8]; // 生成8个随机数 for (int j = 0; j < 8; ++j) vals[j] = dist(rng); // 转成AVX2向量 const __m256i rand_vec = _mm256_loadu_si256(reinterpret_cast<__m256i*>(vals)); // 转浮点计算log,再转回int const __m256 float_vec = _mm256_cvtepi32_ps(rand_vec); const __m256 log_vec = _mm256_log_ps(float_vec); const __m256i result_vec = _mm256_cvtps_epi32(log_vec); // 直接写入内存 _mm256_store_si256(reinterpret_cast<__m256i*>(arr + i), result_vec); } // 处理剩余不足8个的元素 for (; i < size; ++i) { arr[i] = static_cast<int>(log(dist(rng))); } } delete[] arr; }
3. 调整编译选项,榨干硬件性能
gcc没有-O5,实际是-O3,加上针对本机的指令集优化:
g++ -std=c++20 -Ofast -march=native -mavx2 -flto your_code.cpp -lbenchmark -lpthread
-march=native让编译器针对Ryzen 3900XT的所有指令集优化,-flto开启链接时优化,进一步提升代码效率。
4. 去掉多余的内存中转
原来的tmp_arr + memcpy完全没必要,直接计算后写入目标数组,减少一次内存拷贝,编译器也能更好地优化写入流程。
替换rand()后,多线程的锁竞争消失,性能会接近单线程的线性扩展(比如10线程耗时大概10秒左右);加上向量优化后,内存写入吞吐量会进一步提升,backend停顿占比也会降低。
内容的提问来源于stack exchange,提问作者zcoderz

