如何使用现代C++确定超大向量求和的最优线程数
超大向量求和属于典型的内存密集型计算任务,性能瓶颈通常不在CPU算力,而在内存带宽、缓存命中率,所以最优线程数往往不等于CPU逻辑核数,直接用std::thread::hardware_concurrency()拿的数值只能作为参考,不能直接当最优值用。在不知道生产环境配置的前提下,自适应采样调优是成本最低、适配性最好的方案:
第一步:拿到初始线程数范围
先调用std::thread::hardware_concurrency()获取当前机器的逻辑核数作为线程数上限,如果返回值为0(部分嵌入式环境/旧标准库会出现),兜底设置为8即可。线程数的测试范围就设为1到这个上限值即可,超过上限的线程数只会带来额外的调度开销,不会提升性能。第二步:小样本自适应测试
从待求和的超大向量中取1%5%的数据作为测试样本,分别用不同的线程数跑求和任务,统计每个线程数下的耗时,取耗时最短的数值作为最终全局计算的线程数。测试时要注意跑23次取平均值,避免单次CPU调度波动影响结果。
示例代码参考:#include <vector> #include <thread> #include <chrono> #include <numeric> #include <functional> // 单块求和逻辑 void block_sum(const std::vector<long long>& vec, size_t start, size_t end, long long& res) { res = std::accumulate(vec.begin() + start, vec.begin() + end, 0LL); } // 测试指定线程数的耗时,单位微秒 size_t test_thread_cost(const std::vector<long long>& sample, int thread_num) { auto start = std::chrono::high_resolution_clock::now(); std::vector<std::thread> threads; std::vector<long long> part_res(thread_num); size_t block_size = sample.size() / thread_num; for (int i = 0; i < thread_num; ++i) { size_t s = i * block_size; size_t e = (i == thread_num - 1) ? sample.size() : (i + 1) * block_size; threads.emplace_back(block_sum, std::cref(sample), s, e, std::ref(part_res[i])); } for (auto& t : threads) t.join(); // 可加校验逻辑,确保求和结果正确,排除逻辑错误影响测试 auto end = std::chrono::high_resolution_clock::now(); return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count(); } // 获取最优线程数 int get_optimal_thread_num(const std::vector<long long>& full_vec) { int max_thread = std::thread::hardware_concurrency(); max_thread = max_thread == 0 ? 8 : max_thread; // 取样本:最少1000个元素,最多取总长度的5% size_t sample_size = std::min<size_t>(full_vec.size() * 0.05, 100000); sample_size = std::max<size_t>(sample_size, 1000); std::vector<long long> sample(full_vec.begin(), full_vec.begin() + sample_size); int best_thread = 1; size_t min_cost = test_thread_cost(sample, 1); for (int t = 2; t <= max_thread; ++t) { size_t cost = test_thread_cost(sample, t); if (cost < min_cost) { min_cost = cost; best_thread = t; } } return best_thread; }这种采样测试的开销极低,哪怕是TB级的向量,测试耗时也不会超过1毫秒,完全不会影响整体任务的执行效率。
第三步:兜底方案
如果不想做采样测试,也可以直接取逻辑核数的1/2~2/3作为线程数,这个区间的数值对于绝大多数内存密集型场景都能达到接近最优的性能,避免超线程调度冲突、内存带宽不足带来的性能下降。如果向量总长度小于10000个元素,直接用单线程即可,线程创建和调度的开销会远大于并行带来的收益。
额外提示:如果生产环境的机器负载是动态变化的,可以每次程序启动时都跑一次上述的采样调优,不需要把线程数写死在配置中,就能实现全环境的自动适配。C++17之后也可以直接用
std::reduce(std::execution::par, vec.begin(), vec.end())并行求和,标准库实现会做基础的线程数适配,但如果要追求极致的最优性能,自定义采样调优的可控性更高。
内容的提问来源于stack exchange,提问作者ultra

