在现代 C++ 中, 提升程序性能的需求日益增长, 而并行计算和矢量化逐渐成为高性能计算的核心手段. 为了帮助开发者简化并行化操作, C++17 引入了 std::execution
命名空间, 为标准算法提供了一种简单且强大的执行策略机制. 这篇博客将深入介绍 std::execution
的概念, 使用方法及其优势.
什么是 std::execution
?
std::execution
是 C++ 标准库中的一个命名空间, 定义了几种 执行策略(Execution Policies), 用于控制标准算法的执行模式. 这些执行策略可以显式指定算法是以串行, 并行还是矢量化的方式执行.
通过使用 std::execution
, 开发者可以更高效地利用多核 CPU 和 SIMD 指令集, 从而在性能上实现显著提升, 而无需手动管理多线程或矢量化的复杂逻辑.
执行策略概览
std::execution
提供了以下四种执行策略:
-
std::execution::seq
(串行执行):- 算法按默认的顺序依次执行.
- 无并行化, 也不进行矢量化.
-
std::execution::par
(并行执行):- 算法以多线程的方式并行执行.
- 适合多核处理器的计算密集型任务.
-
std::execution::par_unseq
(并行且矢量化执行):- 算法在并行的基础上进一步允许矢量化.
- 利用硬件能力实现更高效的计算.
-
std::execution::unseq
(矢量化执行):- 算法允许矢量化, 但不保证迭代顺序.
- 利用 SIMD 指令提升单线程性能.
std::execution
的使用方法
std::execution
的核心用途是与 C++ 标准算法相结合, 通过简单地传递执行策略作为参数, 指定算法的执行模式.
示例 1: 并行排序
#include <algorithm>
#include <chrono>
#include <execution>
#include <iostream>
#include <random>
#include <vector>int main() {// 生成随机数据集constexpr size_t kTotalCount = 1'000'000;std::vector<int> data(kTotalCount);std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1, kTotalCount);for (auto& d : data) {d = dis(gen);}// 串行排序并计时auto start_seq = std::chrono::high_resolution_clock::now();std::sort(data.begin(), data.end());auto end_seq = std::chrono::high_resolution_clock::now();std::cout << "串行排序耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_seq -start_seq).count()<< " ms" << std::endl;// 打乱数据顺序std::shuffle(data.begin(), data.end(), gen);// 并行排序并计时auto start_par = std::chrono::high_resolution_clock::now();std::sort(std::execution::par, data.begin(), data.end());auto end_par = std::chrono::high_resolution_clock::now();std::cout << "并行排序耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_par -start_par).count()<< " ms" << std::endl;std::shuffle(data.begin(), data.end(), gen);// 矢量化排序并计时auto start_unseq = std::chrono::high_resolution_clock::now();std::sort(std::execution::unseq, data.begin(), data.end());auto end_unseq = std::chrono::high_resolution_clock::now();std::cout << "矢量化排序耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_unseq - start_unseq).count()<< " ms" << std::endl;std::shuffle(data.begin(), data.end(), gen);// 并行且矢量化排序并计时auto start_par_unseq = std::chrono::high_resolution_clock::now();std::sort(std::execution::par_unseq, data.begin(), data.end());auto end_par_unseq = std::chrono::high_resolution_clock::now();std::cout << "并行且矢量化排序耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_par_unseq - start_par_unseq).count()<< " ms" << std::endl;return 0;
}
在这个示例中, std::execution::par
指定 std::sort
可以利用多线程并行化排序操作.
笔者测试了不同的编译器, 结果如下:
- GCC, 未开
-O3
优化串行排序耗时: 348 ms 并行排序耗时: 381 ms 矢量化排序耗时: 381 ms 并行且矢量化排序耗时: 370 ms
- Clang 18.1.3, 未开
-O3
优化串行排序耗时: 355 ms 并行排序耗时: 406 ms 矢量化排序耗时: 419 ms 并行且矢量化排序耗时: 384 ms
- MSVC
串行排序耗时: 309 ms 并行排序耗时: 81 ms 矢量化排序耗时: 321 ms 并行且矢量化排序耗时: 57 ms
可以看到 GCC/Clang 对此方面的优化不是很好, 而 MSVC 则对并行排序优化的比较好.
示例 2: 并行处理数据
#include <execution>
#include <vector>
#include <numeric>
#include <iostream>
#include <chrono>int main() {std::vector<int> data(1'000'000, 1);// 串行归约操作并计时auto start_seq = std::chrono::high_resolution_clock::now();int sum_seq = std::accumulate(data.begin(), data.end(), 0);auto end_seq = std::chrono::high_resolution_clock::now();std::cout << "串行归约耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_seq - start_seq).count()<< " ms" << std::endl;// 并行归约操作并计时auto start_par = std::chrono::high_resolution_clock::now();int sum_par = std::reduce(std::execution::par, data.begin(), data.end(), 0);auto end_par = std::chrono::high_resolution_clock::now();std::cout << "并行归约耗时: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_par - start_par).count()<< " ms" << std::endl;// 输出结果std::cout << "串行归约结果: " << sum_seq << std::endl;std::cout << "并行归约结果: " << sum_par << std::endl;return 0;
}
在上述代码中, std::reduce
使用 std::execution::par
实现了并行化的数据归约(reduce)操作.
输出结果:
- GCC 14.2
串行归约耗时: 14 ms 并行归约耗时: 9 ms
- Clang 18.1.3
串行归约耗时: 16 ms 并行归约耗时: 12 ms
- MSVC
串行归约耗时: 8 ms 并行归约耗时: 4 ms
std::execution
支持的算法
在 C++17 中, 标准库中的许多算法都支持执行策略, 包括但不限于:
std::for_each
: 遍历并处理元素.std::transform
: 对每个元素应用变换操作.std::reduce
: 并行归约操作.std::sort
: 并行排序.
使用建议与注意事项
-
线程安全性:
- 并行执行策略(
par
和par_unseq
)要求代码中的所有数据操作是线程安全的, 尤其是写入共享资源时需要加锁.
- 并行执行策略(
-
适用场景:
- 对于小规模数据集, 串行执行(
seq
)可能性能更好, 因为并行化带来的开销可能超过其带来的收益. - 对于数据独立的计算任务, 并行策略(
par
或par_unseq
)能显著提升性能.
- 对于小规模数据集, 串行执行(
-
硬件依赖:
- 并行化性能提升取决于硬件是否支持多核处理和 SIMD 指令.
-
非确定性行为:
- 并行策略(
par
和par_unseq
)不保证执行顺序, 因此对顺序敏感的任务不适合使用这些策略.
- 并行策略(
性能提升的核心原因
- 并行化: 任务被划分为多个线程执行, 充分利用多核 CPU 的资源.
- 矢量化: 通过 SIMD 指令同时处理多个数据元素, 提升计算密度.
- 任务分解: 标准库会自动将任务分解为多个子任务, 最大化硬件利用率.
总结
std::execution
是 C++17 提供的一个强大工具, 它为标准算法赋予了更灵活的执行模式, 简化了并行计算的实现方式. 通过指定执行策略, 开发者可以更高效地利用硬件资源, 从而显著提升程序性能.
无论是大规模数据处理还是高性能计算场景, std::execution
都是不可或缺的利器. 熟练掌握它, 将为你的 C++ 编程技能增添一份强大的武器.
源码链接
源码链接