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<int> data(1'000'000);
std::mt19937 gen(std::random_device{}());
std::uniform_int_distribution<> dist(1, 1'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<int> data(1'000'000, 1);
// 顺序归约
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或函数不能有未同步的写共享变量,否则会产生竞态。
- • 对顺序敏感的算法使用并行策略:如依赖元素访问顺序的算法,使用
par
或par_unseq
可能导致结果不确定。 - • 小数据集盲目使用并行:线程启动和同步开销可能比顺序执行还大,反而变慢。
- • 不支持的算法或容器:不是所有STL算法都支持执行策略,使用前需确认。
面试可能考察点
- • 解释执行策略的种类及区别。
- • 并行执行策略的底层实现机制。
- • 并行算法中如何保证线程安全和结果一致性。
- • SIMD矢量化的基本原理及其在执行策略中的应用。
- • 何时选择顺序执行,何时选择并行执行。
- • 代码示例中如何正确使用
std::execution
。
总结
C++17的执行策略是现代C++性能优化的利器,它让并行和矢量化算法的调用变得像调用普通算法一样简单。它的设计哲学是“声明你的执行意图,算法库帮你完成复杂细节”,极大降低了并行编程门槛。理解执行策略的不同类型、底层实现和适用场景,是掌握现代C++高性能编程的关键。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1072
文章版权归作者所有,未经允许请勿转载。
THE END