**C++17 并行算法:从 std::for_each 到 std::transform_reduce**

在 C++17 标准中,STL 标准库为并行计算提供了一整套算法接口。通过在算法前加上 std::execution::parstd::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. 性能调优技巧

  1. 数据布局

    • std::vector 连续内存使 CPU 预取更高效。
    • 对于大对象,考虑 std::pmr::vectorstd::vector<std::shared_ptr<T>> 以降低拷贝成本。
  2. 分块大小

    • 默认实现会自动决定子范围大小。若你知道数据量极大,可以手动指定 std::execution::par_unseq 并配合 std::reducepolicy 参数(C++23 才有 std::reduce(par, ...) 的分块控制)来微调。
  3. 避免 False Sharing

    • 并行算法内部使用线程本地缓存(TLP)来减少共享写操作,但如果你自定义 transform 并访问共享结构,请注意内存对齐。
  4. 测试与基准

    • 使用 std::chrono::steady_clock 或 Google Benchmark 进行多次跑测。
    • 对比 parpar_unseqseq 三种策略的速度。

4. 常见陷阱与错误

错误 影响 解决方案
只在单线程上测试 误认为性能提升 在多核机器上跑测,确保 std::execution::par 被激活
线程安全性未考虑 数据竞争导致结果不确定 确保 lambda 只读或使用原子、互斥
过度使用 par_unseq 生成的代码在不支持 SIMD 的平台上无法运行 在不支持的平台上使用 parseq 作为回退
忽略异常传播 并行算法异常会在调用点抛出 使用 std::futurestd::async 包装,以捕获异常

5. 结语

C++17 并行算法为开发者提供了一个“声明式”的并行编程模型。只需在算法前加上执行策略,标准库内部就会完成线程划分、数据分块、并行执行与归约。相较于手写线程或使用 OpenMP,使用标准库的并行算法更易维护、类型安全,也更符合 C++ 的 RAII 设计理念。

小提示:如果你对 C++20 的 Concepts 感兴趣,下一篇文章将介绍如何结合 Concepts 与并行算法,为函数模板添加更精确的约束。祝编码愉快!

发表评论