C++ 标准库并行算法
C++17正式引入了标准库并行算法(后续C++20/C++23有小幅扩展),这些算法基于** 执行策略(Execution Policy) **实现并行控制,无需用户手动管理线程,大幅简化了高性能并行编程的复杂度。并行算法主要分布在<algorithm>(序列算法)和<numeric>(数值算法)头文件中,且必须依赖<execution>头文件提供执行策略支持。
1、核心:执行策略(并行行为的驱动核心)
执行策略是并行算法的第一个参数,用于指定算法的执行模式,直接决定了算法的并行行为和性能表现。C++标准在std::execution命名空间下提供了三种核心执行策略:
1. 顺序执行策略:std::execution::seq
- 特性:本质是串行执行,无任何并行优化,仅作为并行算法的基准(方便串行/并行模式切换测试)。
- 执行方式:单线程按原有串行算法的元素处理顺序执行,无任务拆分、线程调度开销。
- 适用场景:小数据量、调试阶段、存在依赖关系的非并行任务。
2. 并行执行策略:std::execution::par
- 特性:多线程并行执行,标准库管理内置线程池,任务拆分后同步执行。
- 执行方式 :
- 库自动将整体任务拆分为独立子任务(基于数据区间分块);
- 从内置线程池分配线程执行子任务(避免线程频繁创建/销毁的开销);
- 多线程同时执行子任务,算法整体阻塞(直到所有子任务完成才返回);
- 允许子任务按任意顺序执行,但要求无数据竞争(用户保证)。
- 适用场景:大部分并行场景(排序、批量遍历、无依赖转换),是最常用的并行策略。
3. 并行+向量执行策略:std::execution::par_unseq
- 特性 :更激进的并行优化,结合了多线程并行 和单线程SIMD向量执行(如CPU的SSE/AVX指令集,单指令处理多个数据)。
- 执行方式 :
- 同时支持跨线程并行(同
par策略)和线程内SIMD批量执行; - 子任务可能被库重新排序、迁移(跨线程),约束更强;
- 要求子任务完全独立(无任何依赖关系),禁止阻塞操作(如
std::mutex锁定)。
- 同时支持跨线程并行(同
- 适用场景:无依赖、纯计算、大数据量的密集型任务(如大规模数值计算),可获得极致性能。
2、常用并行算法及可运行示例
类别1:非修改式序列算法(std::for_each)
std::for_each是基础并行算法,用于批量遍历并处理元素(只读或线程安全写入),并行版本可大幅提升遍历效率。
用途
批量处理容器元素(打印元素、统计特征、只读转换等)。
完整代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread> // 获取当前线程ID,验证并行性
// 元素处理函数:打印元素值和所属线程ID
void processElement(int val) {
std::cout << "线程ID: " << std::this_thread::get_id()
<< ", 元素值: " << val << std::endl;
}
int main() {
// 初始化测试容器
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 并行执行for_each,使用par策略(第一个参数为执行策略)
std::for_each(std::execution::par, vec.begin(), vec.end(), processElement);
return 0;
}
执行方式解析
- 任务拆分 :标准库将
vec的迭代器区间[begin(), end())拆分为若干子区间(如[1,2,3]、[4,5,6]、[7,8,9,10],粒度由库自动优化); - 线程调度:从内置线程池分配多个线程,每个线程负责一个子区间;
- 并行执行 :多个线程同时调用
processElement处理各自子区间的元素,输出会显示多个不同的线程ID(体现并行性); - 同步返回 :所有元素处理完成后,
std::for_each才会返回,main函数继续执行。
类别2:修改式序列算法
1. std::sort(并行排序)
用途
对容器元素进行排序,大数据量下并行版本性能远超串行std::sort。
完整代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <random> // 生成随机测试数据
int main() {
// 1. 生成10万个随机整数(大数据量体现并行优势)
std::vector<int> vec(100000);
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(1, 10000);
for (int i = 0; i < vec.size(); ++i) {
vec[i] = dis(gen);
}
// 2. 并行排序:使用par策略
std::sort(std::execution::par, vec.begin(), vec.end());
// 3. 验证排序结果(打印前10个元素)
std::cout << "并行排序后前10个元素:";
for (int i = 0; i < 10; ++i) {
std::cout << vec[i] << " ";
}
std::cout << std::endl;
return 0;
}
执行方式解析
- 任务拆分:采用并行分治策略(如并行快速排序/归并排序),将数组拆分为多个子数组;
- 并行排序:多个线程同时排序各自的子数组;
- 结果合并:线程协作合并有序子数组,形成全局有序数组;
- 同步返回 :合并完成后,
std::sort返回,此时vec已完全有序。
2. std::transform(并行数据转换)
用途
批量将输入容器的元素转换后存入输出容器,支持无依赖并行执行。
完整代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
// 转换函数:将元素翻倍
int doubleElement(int val) {
return val * 2;
}
int main() {
// 输入容器
std::vector<int> input = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 输出容器:提前分配空间,避免并行写入时的内存竞争
std::vector<int> output(input.size());
// 并行转换:input元素翻倍后存入output
std::transform(std::execution::par,
input.begin(), input.end(),
output.begin(),
doubleElement);
// 打印转换结果
std::cout << "并行转换后结果:";
for (int val : output) {
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
执行方式解析
- 任务拆分 :将
input和output按相同粒度拆分为子区间; - 并行转换 :多个线程同时处理各自子区间,调用
doubleElement转换元素并写入output对应位置(每个位置仅被一个线程写入,无数据竞争); - 同步返回 :所有元素转换完成后,
std::transform返回。
类别3:数值算法(std::reduce,并行聚合)
注意:
std::accumulate是串行聚合算法,不支持执行策略;std::reduce是其并行版本,要求聚合操作满足结合律。
用途
对容器元素进行聚合计算(累加、乘积等),大数据量下快速聚合。
完整代码
cpp
#include <iostream>
#include <vector>
#include <numeric> // reduce/accumulate头文件
#include <execution>
int main() {
// 1. 生成100万个1的容器(大数据量测试)
std::vector<int> vec(1000000, 1);
// 2. 并行累加:par策略,初始值0,默认加法操作
int parallel_sum = std::reduce(std::execution::par, vec.begin(), vec.end(), 0);
// 3. 串行累加(作为对比)
int serial_sum = std::accumulate(vec.begin(), vec.end(), 0);
// 4. 打印结果
std::cout << "并行累加结果:" << parallel_sum << std::endl;
std::cout << "串行累加结果:" << serial_sum << std::endl;
return 0;
}
执行方式解析
- 任务拆分 :将
vec拆分为若干子区间,每个子区间对应一个局部累加任务; - 并行局部累加:多个线程同时计算各自子区间的局部和(如4个子区间的局部和各为250000);
- 全局汇总:所有局部和计算完成后,库将局部和再次累加(单线程/多线程),得到最终全局和;
- 同步返回 :汇总完成后,
std::reduce返回结果。
- 关键特性:加法满足结合律,因此拆分/汇总顺序不影响结果;若使用减法(不满足结合律),并行结果会与串行结果不一致。
3、并行算法的通用执行流程
无论哪种并行算法,其底层执行都遵循以下5个核心步骤,这是理解并行算法工作机制的关键:
- 任务拆分
标准库根据输入数据规模、容器迭代器类型(随机访问迭代器最优,如vector/array)和执行策略,自动将整体任务拆分为独立无依赖的子任务,拆分方式以「数据区间分块」为主,粒度由库内部优化(无需用户干预)。 - 线程调度
对于par/par_unseq策略,标准库使用内置线程池分配线程(避免线程频繁创建/销毁的开销)。若子任务数量多于线程池线程数,库会进行任务调度(如抢占式调度),让线程完成一个子任务后继续执行下一个。 - 并行执行
par策略:子任务在多个线程上并发执行(真正多线程并行),无固定执行顺序;par_unseq策略:子任务既跨线程并行,又在单线程内通过SIMD向量指令 批量执行(如一次处理8/16个数据),实现双重优化。
此阶段要求用户保证子任务无数据竞争(只读数据安全,独立写入安全,共享写入需同步)。
- 结果汇总(仅聚合类算法)
对于std::reduce等聚合类算法,先计算各子任务的局部结果,再通过聚合操作合并为全局结果,汇总过程可再次并行化,核心依赖操作的结合律。 - 同步返回
所有并行算法均为阻塞式执行 :调用线程会暂停,直到所有子任务执行完成、结果汇总完毕后,才会继续执行。若需要异步并行,需用户结合std::async手动封装。
4、关键注意事项
- 头文件完整性 :必须包含
<execution>(执行策略)+ 对应算法头文件(<algorithm>/<numeric>),否则编译器报错。 - 避免数据竞争 :共享数据写入时,需使用
std::atomic(原子类型)或std::mutex(互斥锁)保证同步;优先使用独立内存写入(如transform的输出容器)。 - 策略选择原则 :
- 优先用
par:兼容性好、约束少,满足大部分场景; - 仅大数据量无依赖场景用
par_unseq:避免约束违规导致未定义行为; - 小数据量用
seq:并行开销(拆分/调度)可能超过收益,串行更快。
- 优先用
- 迭代器影响性能 :随机访问迭代器(
vector/array)并行效率最高;双向迭代器(list/set)并行效率低;输入迭代器(istream_iterator)不支持并行。 - 结合律要求 :
std::reduce等聚合算法要求操作满足结合律,否则结果可能不一致。
总结
- C++17并行算法基于执行策略 (
seq/par/par_unseq)驱动,无需手动管理线程,核心头文件为<execution>+<algorithm>/<numeric>; - 常用并行算法包括
std::for_each(遍历)、std::sort(排序)、std::transform(转换)、std::reduce(聚合),各有明确适用场景; - 并行算法底层执行流程为「任务拆分→线程调度→并行执行→结果汇总(聚合类)→同步返回」;
- 关键优化点:优先使用
par策略、选择随机访问迭代器容器、保证无数据竞争、仅大数据量场景启用并行。
C++17 的三种执行策略研究
已在上个章节的《核心:执行策略(并行行为的驱动核心)》进行了简单阐述,下面详细举例说明
1、std::execution::sequenced_policy(顺序执行策略)
1. 核心特性
- 官方别名 :
std::execution::seq(实际编码中更常用该别名,与完整名称等价) - 执行模式:纯串行执行,无任何并行优化,不创建额外线程
- 核心约束:严格按照容器元素的迭代器顺序处理数据,无任务拆分、无线程调度开销
- 适用场景:小数据量处理、调试阶段(方便跟踪执行流程)、元素处理存在依赖关系(无法并行)、并行开销大于收益的场景
2. 完整示例
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
// 元素处理函数:打印元素值和所属线程ID
void processSeqElement(int val) {
// 打印线程ID,验证是否为单线程执行
std::cout << "线程ID: " << std::this_thread::get_id()
<< " | 处理元素: " << val
<< "(顺序执行,按元素索引递增处理)" << std::endl;
}
int main() {
// 初始化测试容器
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用顺序执行策略(完整名称/std::execution::seq别名均可)
std::for_each(std::execution::sequenced_policy(), // 完整策略名称
vec.begin(), vec.end(),
processSeqElement);
// 等价写法(更简洁,实际开发优先使用)
// std::for_each(std::execution::seq, vec.begin(), vec.end(), processSeqElement);
return 0;
}
3. 代码与执行机制解析
- 代码说明 :
- 采用
std::for_each算法,第一个参数传入std::execution::sequenced_policy()(完整名称)或其别名std::execution::seq; - 处理函数
processSeqElement打印当前线程ID和元素值,用于验证串行特性; - 测试容器
vec包含5个有序元素,便于观察执行顺序。
- 采用
- 执行机制 :
- 无任务拆分:标准库不会对
vec的迭代器区间进行拆分,整体作为一个单任务处理; - 单线程执行:仅使用调用线程(
main线程)执行所有元素的处理逻辑,输出中所有元素对应的线程ID完全一致; - 严格按序执行:元素会按照
1→2→3→4→5的顺序依次处理,不会出现乱序输出,完全遵循迭代器的遍历顺序; - 无额外开销:无需线程创建、调度、同步等开销,执行流程与传统串行算法完全一致。
- 无任务拆分:标准库不会对
2、std::execution::parallel_policy(并行执行策略)
1. 核心特性
- 官方别名 :
std::execution::par(实际编码中更常用该别名) - 执行模式:多线程并行执行,由标准库管理内置线程池(避免线程频繁创建/销毁的开销)
- 核心约束 :
- 子任务可按任意顺序执行(元素处理顺序不保证);
- 用户需保证元素处理无数据竞争(如共享数据写入需同步);
- 算法为阻塞式执行(调用线程等待所有子任务完成后返回)。
- 适用场景:大部分通用并行场景(如大数据量排序、批量遍历、无依赖数据转换),是日常并行编程的首选策略。
2. 完整示例
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <thread>
#include <random>
// 元素处理函数:打印元素值和所属线程ID
void processParElement(int val) {
std::cout << "线程ID: " << std::this_thread::get_id()
<< " | 处理元素: " << val << std::endl;
}
int main() {
// 初始化10个元素的测试容器(数据量适中,便于观察并行效果)
std::vector<int> vec(10);
// 生成随机数填充容器(打乱初始顺序,更易观察并行乱序执行)
std::random_device rd;
std::mt19937 gen(rd());
std::iota(vec.begin(), vec.end(), 1); // 先填充1-10
std::shuffle(vec.begin(), vec.end(), gen); // 打乱顺序
std::cout << "=== 并行执行策略(par)开始处理 ===" << std::endl;
// 使用并行执行策略(别名std::execution::par,更简洁)
std::for_each(std::execution::par,
vec.begin(), vec.end(),
processParElement);
std::cout << "=== 并行执行策略(par)处理完成 ===" << std::endl;
return 0;
}
3. 代码与执行机制解析
- 代码说明 :
- 采用
std::execution::par别名(实际开发首选,代码更简洁); - 通过
std::shuffle打乱容器元素顺序,便于观察并行执行的乱序特性; - 处理函数打印线程ID,直观验证多线程并行效果。
- 采用
- 执行机制 :
- 任务拆分 :标准库自动将
vec的迭代器区间[begin(), end())拆分为若干独立子区间(如[3,7,1]、[10,2,5]、[4,6,8,9],拆分粒度由库自动优化); - 线程调度:从内置线程池分配多个线程(数量通常与CPU核心数匹配),每个线程负责一个子区间的处理任务;
- 并行执行 :多个线程同时调用
processParElement处理各自子区间的元素,输出中会出现多个不同的线程ID,且元素处理顺序混乱(无固定顺序); - 同步返回 :所有子区间的元素处理完成后,
std::for_each才会返回,main函数继续执行后续逻辑(阻塞式执行); - 无额外线程开销:依赖内置线程池复用线程,避免了频繁创建/销毁线程的性能损耗。
- 任务拆分 :标准库自动将
3、std::execution::parallel_unsequenced_policy(并行+向量执行策略)
1. 核心特性
- 官方别名 :
std::execution::par_unseq(实际编码中更常用该别名) - 执行模式:最激进的性能优化,结合了「多线程并行」和「单线程SIMD向量执行」(如CPU的SSE/AVX指令集,单指令同时处理多个数据)
- 核心约束 (比
parallel_policy更严格):- 子任务完全独立(无任何数据依赖、无跨任务同步);
- 禁止阻塞操作(如
std::mutex锁定、线程休眠等); - 子任务可能被标准库重新排序、跨线程迁移;
- 仅支持随机访问迭代器(如
vector/array,不支持list/set)。
- 适用场景:大数据量、纯计算、无依赖的密集型任务(如大规模数值转换、批量数学计算),追求极致执行性能。
2. 完整示例
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>
// 纯计算转换函数:元素平方(无依赖、无阻塞、纯CPU计算,适配par_unseq)
int squareElement(int val) {
return val * val;
}
int main() {
// 初始化100万个元素的大数据量容器(体现par_unseq的性能优势)
std::vector<int> input(1000000);
std::iota(input.begin(), input.end(), 1); // 填充1-1000000
std::vector<int> output(input.size()); // 提前分配输出空间,避免内存竞争
std::cout << "=== 并行+向量执行策略(par_unseq)开始转换 ===" << std::endl;
// 使用并行+向量执行策略(别名std::execution::par_unseq)
std::transform(std::execution::par_unseq,
input.begin(), input.end(),
output.begin(),
squareElement);
std::cout << "=== 并行+向量执行策略(par_unseq)转换完成 ===" << std::endl;
// 验证前10个和后10个元素的转换结果
std::cout << "前10个元素转换结果:" << std::endl;
for (int i = 0; i < 10; ++i) {
std::cout << input[i] << " → " << output[i] << std::endl;
}
std::cout << "后10个元素转换结果:" << std::endl;
for (int i = input.size() - 10; i < input.size(); ++i) {
std::cout << input[i] << " → " << output[i] << std::endl;
}
return 0;
}
3. 代码与执行机制解析
- 代码说明 :
- 采用
std::execution::par_unseq别名,选择std::transform算法(纯数据转换,无依赖); - 测试容器大小为100万(大数据量才能体现SIMD向量执行的优势);
- 转换函数
squareElement为纯计算逻辑(无阻塞、无数据依赖),完全满足par_unseq的严格约束; - 提前分配输出容器
output的空间,每个元素仅被一个线程写入,避免数据竞争。
- 采用
- 执行机制 :
- 双层并行优化 :
- 跨线程并行:同
par策略,将输入区间拆分为子区间,由多个线程并行处理; - 线程内SIMD向量执行:单个线程通过CPU向量指令(如AVX-512),一次处理8/16个元素(而非逐个处理),大幅提升单线程处理效率;
- 跨线程并行:同
- 灵活任务调度:标准库可根据系统负载,动态重新排序子任务、将子任务从一个线程迁移到另一个线程,最大化CPU利用率;
- 无阻塞约束:由于禁止阻塞操作,线程不会陷入等待,CPU核心始终处于高负载状态,保证极致性能;
- 同步返回 :与
par策略一致,所有元素转换完成后,算法阻塞返回,输出容器output完全有效。
- 双层并行优化 :
4、三种执行策略核心对比总结
| 执行策略完整名称 | 常用别名 | 执行模式 | 线程特性 | 核心约束 | 适用场景 |
|---|---|---|---|---|---|
sequenced_policy |
std::execution::seq |
纯串行执行 | 单线程(调用线程) | 严格按迭代器顺序执行 | 小数据量、调试、有依赖的任务 |
parallel_policy |
std::execution::par |
多线程并行执行 | 多线程(内置线程池) | 无数据竞争、允许乱序执行 | 大部分通用并行场景(排序、遍历等) |
parallel_unsequenced_policy |
std::execution::par_unseq |
多线程并行+SIMD向量执行 | 多线程+SIMD优化 | 无依赖、无阻塞、随机访问迭代器 | 大数据量、纯计算、极致性能需求 |
关键补充
- 三种策略均定义在
<execution>头文件中,使用时必须包含该头文件; - 实际编码中,优先使用别名(
seq/par/par_unseq),代码更简洁易读; - 策略选择优先级:
par(通用场景)>seq(调试/小数据)>par_unseq(极致性能/无依赖)。
5、 20 std::execution::unsequenced_policy 策略
你需要了解C++20新增的std::execution::unsequenced_policy执行策略,该策略是对C++17并行执行策略的补充,主打「单线程+SIMD向量优化」,在特定场景下能以极低开销实现性能提升,下面将从核心特性、完整示例、执行机制及与其他策略的对比展开详细说明。
1、std::execution::unsequenced_policy 核心特性
1. 基础信息
- 完整名称 :
std::execution::unsequenced_policy - 常用别名 :
std::execution::unseq(实际编码中优先使用该别名,更简洁易读) - 所属标准:C++20(需编译器支持C++20及以上标准,如GCC 10+、Clang 11+、MSVC 19.20+)
- 核心头文件 :
<execution>(使用时必须包含该头文件,同时搭配算法头文件<algorithm>/<numeric>)
2. 核心执行模式
区别于C++17的三种策略,unsequenced_policy的核心是「单线程执行 + SIMD向量优化」,具体特性如下:
- 单线程特性 :不创建任何额外线程,仅使用调用线程(如
main线程)执行任务,无线程创建、调度、同步的开销; - SIMD向量优化:利用CPU的SIMD指令集(如SSE、AVX、AVX-512),实现「单指令多数据」批量处理,单个CPU指令可同时处理8/16个同类型元素(如一次计算8个整数的平方),大幅提升单线程处理效率;
- 执行顺序不保证 :虽然是单线程执行,但由于SIMD批量处理和库内部优化,元素的处理顺序不严格遵循迭代器顺序 (与
seq的严格按序执行形成鲜明对比); - 严格约束条件 (比
seq严格,略低于par_unseq):- 元素处理必须完全独立(无数据依赖,如不能根据前一个元素的值修改当前元素);
- 禁止阻塞操作(如
std::mutex锁定、std::this_thread::sleep_for等); - 仅支持随机访问迭代器 (如
std::vector、std::array、原生数组,不支持std::list、std::set等双向/输入迭代器容器); - 无数据竞争(由于单线程执行,只读数据天然安全;写入数据时,每个内存位置仅被单次写入,无需额外同步)。
3. 适用场景
- 小到中等数据量的纯计算任务(数据量过小时,SIMD优化的收益大于开销;数据量过大时,建议使用
par_unseq); - 对线程开销敏感的场景(如嵌入式系统、低功耗设备,线程创建/调度的开销可能抵消并行收益);
- 无法使用多线程的场景(如任务存在轻微线程安全风险,或系统线程资源紧张);
- 纯数据转换/计算场景(如元素平方、数值累加、数据格式化等无依赖逻辑)。
2、完整可运行示例
下面以std::transform算法为例(纯数据转换,适配unseq策略的约束),展示unsequenced_policy的使用方式,代码可直接编译运行。
示例代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>
#include <thread>
// 纯计算转换函数:计算元素的平方(无数据依赖、无阻塞、纯CPU操作,适配unseq策略)
int squareElement(int val) {
return val * val;
}
int main() {
// 1. 初始化测试容器:10万个整数(小到中等数据量,体现unseq的优势)
std::vector<int> input(100000);
std::iota(input.begin(), input.end(), 1); // 填充1~100000
std::vector<int> output(input.size()); // 提前分配输出空间,避免内存竞争
// 2. 打印调用线程ID(验证unseq的单线程特性)
std::cout << "调用线程ID:" << std::this_thread::get_id() << std::endl;
std::cout << "=== 开始使用unseq策略执行并行转换 ===" << std::endl;
// 3. 使用unsequenced_policy(别名std::execution::unseq)执行transform
std::transform(std::execution::unseq,
input.begin(), input.end(),
output.begin(),
squareElement);
std::cout << "=== unseq策略转换完成 ===" << std::endl;
// 4. 验证转换结果(打印前10个和后10个元素,确认正确性)
std::cout << "前10个元素转换结果:" << std::endl;
for (int i = 0; i < 10; ++i) {
std::cout << input[i] << " → " << output[i] << "(预期值:" << input[i] * input[i] << ")" << std::endl;
}
std::cout << "后10个元素转换结果:" << std::endl;
for (int i = input.size() - 10; i < input.size(); ++i) {
std::cout << input[i] << " → " << output[i] << "(预期值:" << input[i] * input[i] << ")" << std::endl;
}
return 0;
}
编译与运行说明
- 编译命令(GCC):
g++ -std=c++20 main.cpp -o unseq_demo(需指定C++20标准) - 运行结果:所有转换结果与预期值一致,且仅输出一个线程ID(验证单线程特性),转换效率高于串行
seq策略。
3、执行机制解析
std::execution::unseq的执行流程简单清晰,无跨线程调度逻辑,核心分为3步:
1. 任务批量划分(单线程内)
标准库根据CPU的SIMD指令集宽度(如AVX-512对应8个int类型元素),将输入迭代器区间[begin(), end())划分为若干批量数据块(而非跨线程子区间)。例如,对于10万个int元素,会划分为12500个批量块(每个块8个元素),批量大小由库自动适配CPU特性,无需用户干预。
2. SIMD向量并行执行(单线程内)
调用线程依次处理每个批量数据块,通过CPU的SIMD向量指令实现「批量计算」:
- 传统串行(
seq策略):逐个处理元素,每个指令仅处理1个int的平方计算; unseq策略:批量处理元素,单个SIMD指令同时处理8个int的平方计算,相当于单线程内的「数据级并行」,大幅提升处理速度。- 注意:批量块的处理顺序可能与迭代器顺序一致,但块内元素的处理是并行的,且整体元素的处理顺序不保证严格按迭代器索引递增(库可能调整批量块顺序以优化缓存命中率)。
3. 同步返回(单线程阻塞)
与其他执行策略一致,std::transform为阻塞式执行:调用线程会依次处理所有批量数据块,直到所有元素转换完成后,算法才会返回,后续代码才能继续执行。由于无跨线程同步,其阻塞开销远低于par和par_unseq策略。
4、unseq与其他执行策略的核心对比
为了更清晰地理解unseq的定位,下面将其与C++17的三种策略进行核心对比:
| 执行策略(C++版本) | 执行模式 | 线程数量 | 执行顺序 | 核心优势 | 适用场景 |
|---|---|---|---|---|---|
seq(C++17) |
纯串行执行(无SIMD) | 1个 | 严格按迭代器顺序 | 无开销、可调试、支持依赖 | 小数据量、有依赖、调试阶段 |
unseq(C++20) |
单线程+SIMD向量执行 | 1个 | 不保证严格顺序 | 低开销、单线程安全、速度快 | 小中等数据量、纯计算、线程敏感场景 |
par(C++17) |
多线程并行(无SIMD) | 多个 | 不保证顺序 | 通用并行、兼容性好 | 大部分大数据量并行场景 |
par_unseq(C++17) |
多线程+SIMD向量执行 | 多个 | 不保证顺序 | 极致性能、双重并行优化 | 超大数据量、纯计算、无依赖场景 |
关键差异总结
unseqvsseq:两者均为单线程,但unseq有SIMD向量优化,速度更快;seq严格按序执行,支持元素依赖;unseqvspar_unseq:两者均有SIMD优化,但unseq是单线程(无线程开销),par_unseq是多线程(有线程调度开销),小数据量下unseq更优,大数据量下par_unseq更优;unseqvspar:unseq单线程+SIMD,par多线程无SIMD,纯计算场景下unseq的单线程SIMD可能媲美甚至超过par的多线程(无线程开销)。
5、使用注意事项
- 编译器支持 :必须使用支持C++20的编译器,且部分编译器可能需要开启SIMD优化开关(如GCC的
-mavx2、-mavx512f); - 迭代器限制 :仅支持随机访问迭代器,使用
list、map等容器会编译报错; - 约束遵守:必须保证元素处理无数据依赖、无阻塞操作,否则会导致未定义行为;
- 数据量选择 :小到中等数据量(如1万~100万元素)优先使用
unseq,超大数据量(如1000万+元素)优先使用par_unseq; - 性能测试 :不同CPU的SIMD支持程度不同,实际使用时建议通过性能测试对比
unseq、seq、par的效果。
总结
std::execution::unsequenced_policy(别名unseq)是C++20新增单线程执行策略,核心为「单线程+SIMD向量优化」;- 其无线程开销、单线程安全,适合小中等数据量、纯计算、线程敏感的场景,执行顺序不保证,仅支持随机访问迭代器;
- 与
seq相比有SIMD性能优势,与par_unseq相比有低开销优势,是C++执行策略中的「轻量级性能优化选项」; - 实际编码中优先使用别名
std::execution::unseq,搭配std::transform、std::reduce等算法实现高效单线程批量计算。
算法穷举
1、 列举一下
1. 排序算法
cpp
std::sort(std::execution::par, begin, end);
std::stable_sort(std::execution::par, begin, end);
std::partial_sort(std::execution::par, begin, middle, end);
std::nth_element(std::execution::par, begin, nth, end);
含义:对范围进行排序,并行版本可加速大规模数据排序。
注意事项:
- 比较函数必须线程安全
- 不稳定排序的相对顺序可能因并行化而改变
- 数据竞争需避免
2. 数值算法
cpp
std::reduce(std::execution::par, begin, end, init);
std::transform_reduce(std::execution::par, begin1, end1, begin2, init);
std::inclusive_scan(std::execution::par, begin, end, result);
std::exclusive_scan(std::execution::par, begin, end, result, init);
std::transform_inclusive_scan(std::execution::par, ...);
std::transform_exclusive_scan(std::execution::par, ...);
含义:
reduce:并行版本的累加,要求操作满足结合律transform_reduce:先变换再累加scan:前缀和计算
注意事项:
- 操作必须满足结合律
- 浮点运算顺序可能改变,影响精度
- 避免副作用
3. 查找算法
cpp
std::find(std::execution::par, begin, end, value);
std::find_if(std::execution::par, begin, end, predicate);
std::find_if_not(std::execution::par, begin, end, predicate);
std::find_end(std::execution::par, begin1, end1, begin2, end2);
std::find_first_of(std::execution::par, begin1, end1, begin2, end2);
std::search(std::execution::par, begin1, end1, begin2, end2);
std::search_n(std::execution::par, begin, end, count, value);
std::count(std::execution::par, begin, end, value);
std::count_if(std::execution::par, begin, end, predicate);
std::all_of(std::execution::par, begin, end, predicate);
std::any_of(std::execution::par, begin, end, predicate);
std::none_of(std::execution::par, begin, end, predicate);
std::adjacent_find(std::execution::par, begin, end);
std::mismatch(std::execution::par, begin1, end1, begin2);
std::equal(std::execution::par, begin1, end1, begin2);
注意事项:
- 谓词必须是纯函数(无副作用)
- 找到第一个匹配项后可能继续搜索其他部分
- 返回顺序不确定,但保证正确性
4. 变换算法
cpp
std::for_each(std::execution::par, begin, end, function);
std::for_each_n(std::execution::par, begin, n, function);
std::transform(std::execution::par, begin1, end1, result, unary_op);
std::transform(std::execution::par, begin1, end1, begin2, result, binary_op);
含义:并行处理每个元素。
注意事项:
- 函数对象必须是线程安全的
- 元素处理顺序不确定
- 避免数据竞争
5. 复制/移动算法
cpp
std::copy(std::execution::par, begin, end, result);
std::copy_n(std::execution::par, begin, n, result);
std::copy_if(std::execution::par, begin, end, result, predicate);
std::move(std::execution::par, begin, end, result);
注意事项:
- 源和目标范围不能重叠(除非是开头)
- 并行复制可能提高大内存块传输速度
6. 填充算法
cpp
std::fill(std::execution::par, begin, end, value);
std::fill_n(std::execution::par, begin, n, value);
std::generate(std::execution::par, begin, end, generator);
std::generate_n(std::execution::par, begin, n, generator);
注意事项:
- 生成器函数必须是线程安全的
- 调用顺序不确定
7. 替换算法
cpp
std::replace(std::execution::par, begin, end, old_val, new_val);
std::replace_if(std::execution::par, begin, end, predicate, new_val);
std::replace_copy(std::execution::par, begin, end, result, old_val, new_val);
std::replace_copy_if(std::execution::par, begin, end, result, predicate, new_val);
8. 移除算法
cpp
std::remove(std::execution::par, begin, end, value);
std::remove_if(std::execution::par, begin, end, predicate);
std::remove_copy(std::execution::par, begin, end, result, value);
std::remove_copy_if(std::execution::par, begin, end, result, predicate);
std::unique(std::execution::par, begin, end);
std::unique_copy(std::execution::par, begin, end, result);
注意事项:
- 相对顺序可能改变
- 通常需要配合
erase使用
9. 划分算法
cpp
std::partition(std::execution::par, begin, end, predicate);
std::partition_copy(std::execution::par, begin, end, out_true, out_false, predicate);
std::is_partitioned(std::execution::par, begin, end, predicate);
注意事项:
- 不保证稳定划分(相等元素相对顺序可能改变)
10. 合并算法
cpp
std::merge(std::execution::par, begin1, end1, begin2, end2, result);
11. 集合算法
cpp
std::includes(std::execution::par, begin1, end1, begin2, end2);
std::set_union(std::execution::par, begin1, end1, begin2, end2, result);
std::set_intersection(std::execution::par, begin1, end1, begin2, end2, result);
std::set_difference(std::execution::par, begin1, end1, begin2, end2, result);
std::set_symmetric_difference(std::execution::par, begin1, end1, begin2, end2, result);
12. 堆操作
cpp
std::is_heap(std::execution::par, begin, end);
std::is_heap_until(std::execution::par, begin, end);
13. 最值算法
cpp
std::min_element(std::execution::par, begin, end);
std::max_element(std::execution::par, begin, end);
std::minmax_element(std::execution::par, begin, end);
14. 其他算法
cpp
std::reverse(std::execution::par, begin, end);
std::reverse_copy(std::execution::par, begin, end, result);
std::rotate(std::execution::par, begin, middle, end);
std::rotate_copy(std::execution::par, begin, middle, end, result);
std::is_sorted(std::execution::par, begin, end);
std::is_sorted_until(std::execution::par, begin, end);
2、重要注意事项
1. 线程安全性要求
cpp
// 错误示例:非线程安全的函数对象
int counter = 0;
std::for_each(std::execution::par, v.begin(), v.end(),
[&](auto& x) { ++counter; }); // 数据竞争!
// 正确做法:使用原子操作或避免共享状态
std::atomic<int> atomic_counter{0};
std::for_each(std::execution::par, v.begin(), v.end(),
[&](auto& x) { ++atomic_counter; });
2. 异常处理
cpp
try {
std::sort(std::execution::par, v.begin(), v.end());
} catch(...) {
// 并行算法可能抛出多个异常
// 实际可能调用std::terminate()
}
3. 性能考虑
- 数据规模:小数据可能因并行开销而变慢
- 内存局部性:并行可能破坏缓存友好性
- 负载均衡:确保任务分配均匀
4. 算法特定要求
cpp
// reduce要求操作满足结合律
std::reduce(std::execution::par, v.begin(), v.end(), 0.0,
[](double a, double b) { return a + b; }); // 浮点数不严格满足结合律
5. 执行策略选择指南
cpp
// 小数据集 - 顺序执行
std::sort(std::execution::seq, small.begin(), small.end());
// 计算密集型 - 并行执行
std::transform(std::execution::par, large.begin(), large.end(),
result.begin(), complex_operation);
// SIMD友好操作 - 并行+向量化
std::for_each(std::execution::par_unseq, data.begin(), data.end(),
[](auto& x) { x *= 2; });
并行算法是C++17的重要特性,正确使用可以显著提升性能,但需要仔细考虑线程安全和性能影响。