在 C++17 标准中,STL 标准库为并行计算提供了一整套算法接口。通过在算法前加上 std::execution::par 或 std::execution::par_unseq,即可让算法在多核 CPU 上并行执行,而无需手写线程或 OpenMP。本文将从最常见的 std::for_each 开始,逐步演示 std::transform_reduce 的使用,并讨论并行算法的性能调优与注意事项。
1. 并行执行策略
| 策略 | 含义 | 适用场景 |
|---|---|---|
std::execution::seq |
顺序执行 | 默认行为,兼容所有平台 |
std::execution::par |
仅使用多线程 | 需要线程并行但不想使用 SIMD |
std::execution::par_unseq |
多线程+SIMD | 需要尽可能多的硬件并行,但可能不在所有平台支持 |
示例
#include <vector>
#include <numeric>
#include <execution>
#include <iostream>
int main() {
std::vector <int> v(1'000'000, 1);
auto sum = std::reduce(std::execution::par, v.begin(), v.end(), 0);
std::cout << "sum = " << sum << '\n';
}
2. 逐步演示
2.1 std::for_each
std::for_each 在并行策略下会把迭代器范围拆分成若干子范围,每个子范围在单独线程中处理。适合做无返回值的副作用操作。
std::vector <int> v(1'000'000, 1);
std::for_each(std::execution::par, v.begin(), v.end(),
[](int& x){ x += 2; });
注意:并行
for_each的闭包(lambda)必须是线程安全的。若对共享数据做写操作,必须使用原子或锁。
2.2 std::transform
std::transform 同样支持并行,适用于把每个元素映射为另一个元素。
std::vector <int> src(1'000'000, 2);
std::vector <int> dst(1'000'000);
std::transform(std::execution::par, src.begin(), src.end(),
dst.begin(), [](int x){ return x * x; });
2.3 std::transform_reduce
std::transform_reduce 在单个调用中完成映射与归约。它是 std::accumulate + std::transform 的组合,支持并行与并行+SIMD。
std::vector <int> v(1'000'000, 3);
auto result = std::transform_reduce(
std::execution::par, // 并行策略
v.begin(), v.end(), // 输入范围
0, // 初始值
std::plus<>(), // 归约操作
[](int x){ return x * x; } // 映射操作
);
std::cout << "sum of squares = " << result << '\n';
核心优势
- 仅一次循环访问数据
- 编译器可更好地内联并行化
- 代码更简洁易读
3. 性能调优技巧
-
数据布局
std::vector连续内存使 CPU 预取更高效。- 对于大对象,考虑
std::pmr::vector或std::vector<std::shared_ptr<T>>以降低拷贝成本。
-
分块大小
- 默认实现会自动决定子范围大小。若你知道数据量极大,可以手动指定
std::execution::par_unseq并配合std::reduce的policy参数(C++23 才有std::reduce(par, ...)的分块控制)来微调。
- 默认实现会自动决定子范围大小。若你知道数据量极大,可以手动指定
-
避免 False Sharing
- 并行算法内部使用线程本地缓存(TLP)来减少共享写操作,但如果你自定义
transform并访问共享结构,请注意内存对齐。
- 并行算法内部使用线程本地缓存(TLP)来减少共享写操作,但如果你自定义
-
测试与基准
- 使用
std::chrono::steady_clock或 Google Benchmark 进行多次跑测。 - 对比
par、par_unseq、seq三种策略的速度。
- 使用
4. 常见陷阱与错误
| 错误 | 影响 | 解决方案 |
|---|---|---|
| 只在单线程上测试 | 误认为性能提升 | 在多核机器上跑测,确保 std::execution::par 被激活 |
| 线程安全性未考虑 | 数据竞争导致结果不确定 | 确保 lambda 只读或使用原子、互斥 |
过度使用 par_unseq |
生成的代码在不支持 SIMD 的平台上无法运行 | 在不支持的平台上使用 par 或 seq 作为回退 |
| 忽略异常传播 | 并行算法异常会在调用点抛出 | 使用 std::future 或 std::async 包装,以捕获异常 |
5. 结语
C++17 并行算法为开发者提供了一个“声明式”的并行编程模型。只需在算法前加上执行策略,标准库内部就会完成线程划分、数据分块、并行执行与归约。相较于手写线程或使用 OpenMP,使用标准库的并行算法更易维护、类型安全,也更符合 C++ 的 RAII 设计理念。
小提示:如果你对 C++20 的 Concepts 感兴趣,下一篇文章将介绍如何结合 Concepts 与并行算法,为函数模板添加更精确的约束。祝编码愉快!