2.6、执行策略(std::execution)

什么是执行策略(std::execution)?

简单来说,执行策略就是告诉标准库算法“你用什么方式来执行这段代码”。传统的STL算法都是顺序执行的,也就是单线程、一个元素接着一个元素处理。C++17新增的执行策略允许你告诉算法:“用多线程并行执行”,或者“用SIMD指令矢量化执行”,甚至两者结合。

它定义在<execution>头文件里,主要有三种(C++20又加了一种):

  • • std::execution::seq:顺序执行,传统方式,单线程,保证顺序,最安全。
  • • std::execution::par:并行执行,算法可以用多线程分块处理数据,适合大数据量且元素间操作独立的场景。
  • • std::execution::par_unseq:并行且允许矢量化执行,结合多线程和SIMD指令,性能最高,但对代码要求更严格。
  • • std::execution::unseq(C++20引入):只允许矢量化,不使用多线程,适合单线程下利用SIMD提升性能。

这四种策略,分别对应不同的性能和安全权衡,选择合适的策略是发挥性能的关键。

设计哲学与底层原理

执行策略的设计哲学是“让算法调用者声明意图,而不是自己写并行代码”。这符合现代C++追求的高效抽象和零开销原则。底层实现通常由标准库或编译器提供支持:

  • • 顺序策略:调用传统算法实现,保证顺序执行。
  • • 并行策略:标准库会根据硬件线程数,自动分割数据区间,启动多个线程并行执行子任务,最后合并结果。
  • • 矢量化策略:利用CPU的SIMD指令集(如AVX、SSE)对数据批量处理,提升单线程性能。
  • • 并行+矢量化:结合上述两者,既多线程又SIMD。

底层实现需要保证线程安全,避免数据竞争,且并行策略不保证执行顺序,代码必须无副作用或副作用可控。

典型案例讲解

下面用一个并行排序和并行归约的案例,结合底层细节讲解执行策略的用法与原理。

案例1:并行排序

#include <vector>
#include <algorithm>
#include <execution>
#include <iostream>
#include <random>

int main() {
    std::vector<intdata(1'000'000);
    std::mt19937 gen(std::random_device{}());
    std::uniform_int_distribution<> dist(11'000'000);
    for (auto& d : data) d = dist(gen);

    // 顺序排序
    std::sort(std::execution::seq, data.begin(), data.end());

    // 并行排序
    std::sort(std::execution::par, data.begin(), data.end());

    // 并行且矢量化排序
    std::sort(std::execution::par_unseq, data.begin(), data.end());

    return 0;
}

底层细节:

  • • std::execution::seq调用传统单线程快速排序。
  • • std::execution::par会将data分成多个区间,启动线程池中的多个线程分别排序各区间,最后合并排序结果(归并)。
  • • std::execution::par_unseq除了多线程,还会利用SIMD指令加速区间内排序的比较和交换操作。

设计亮点:

  • • 你只需传入策略参数,算法内部自动选择合适执行路径,极大简化并行编程。
  • • 并行排序对线程安全要求高,算法保证不破坏数据一致性。
  • • 适合大数据量,线程启动和同步开销在数据量大时被摊薄。

案例2:并行归约(求和)

#include <vector>
#include <numeric>
#include <execution>
#include <iostream>

int main() {
    std::vector<intdata(1'000'0001);

    // 顺序归约
    int sum_seq = std::reduce(std::execution::seq, data.begin(), data.end());

    // 并行归约
    int sum_par = std::reduce(std::execution::par, data.begin(), data.end());

    std::cout << "顺序和: " << sum_seq << "\n并行和: " << sum_par << std::endl;
    return 0;
}

底层细节:

  • • 顺序执行时,std::reduce等同于std::accumulate,一个元素一个元素累加。
  • • 并行执行时,数据被分块,每个线程计算局部和,最后主线程合并局部和。
  • • 归约操作必须满足结合律,保证并行计算结果与顺序一致。

常见错误与面试考点

常见错误

  • • 副作用导致数据竞争:并行策略下,传入的Lambda或函数不能有未同步的写共享变量,否则会产生竞态。
  • • 对顺序敏感的算法使用并行策略:如依赖元素访问顺序的算法,使用parpar_unseq可能导致结果不确定。
  • • 小数据集盲目使用并行:线程启动和同步开销可能比顺序执行还大,反而变慢。
  • • 不支持的算法或容器:不是所有STL算法都支持执行策略,使用前需确认。

面试可能考察点

  • • 解释执行策略的种类及区别。
  • • 并行执行策略的底层实现机制。
  • • 并行算法中如何保证线程安全和结果一致性。
  • • SIMD矢量化的基本原理及其在执行策略中的应用。
  • • 何时选择顺序执行,何时选择并行执行。
  • • 代码示例中如何正确使用std::execution

总结

C++17的执行策略是现代C++性能优化的利器,它让并行和矢量化算法的调用变得像调用普通算法一样简单。它的设计哲学是“声明你的执行意图,算法库帮你完成复杂细节”,极大降低了并行编程门槛。理解执行策略的不同类型、底层实现和适用场景,是掌握现代C++高性能编程的关键。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END