【C++】transform, reduce, scan是什么意思?理解常用并行算法及其实现原理
文章目录
- 从并发到并行
- 并发和并行的区别
- 因特尔开源的并行编程库:TBB
- 基于 TBB 的版本:任务组
- parallel_invoke
- 并行循环
- 时间复杂度(time-efficiency)与工作量复杂度(work-efficiency)
- 映射(map)
- parallel_for
- 基于迭代器区间:parallel_for_each
- 二维区间上的 for 循环:blocked_range2d
- 三维区间上的 for 循环:blocked_range3d
- 所有区间类型
- 缩并与扫描
- 缩并(reduce)
- 并行缩并
- 性能测试
- 参考
从并发到并行
并发和并行的区别
运用多线程的方式和动机,一般分为两种。
并发:单核处理器,操作系统通过时间片调度算法,轮换着执行着不同的线程,看起来就好像是同时运行一样,其实每一时刻只有一个线程在运行。目的:异步地处理多个不同的任务,避免同步造成的阻塞。
并行:多核处理器,每个处理器执行一个线程,真正的同时运行。目的:将一个任务分派到多个核上,从而更快完成任务。
因特尔开源的并行编程库:TBB
- link
安装 TBB
Ubuntu:
sudo apt-get install libtbb-dev
Arch Linux:
sudo pacman -S tbb
Windows:
.\vcpkg install tbb:x64-windows
Mac OS:
.\vcpkg install tbb:x64-macos
Other:
从源码构建安装,参考:https://blog.csdn.net/weixin_42973508/article/details/111681426
安装第三方库-包管理器
·Linux可以用系统自带的包管理器(如apt)安装C++包。
·>pacman-S fmt·Windows则没有自带的包管理器。因此可以用跨平台的vcpkg:
https://github.com/microsoft/vcpkg
·使用方法:下载vcpkg的源码,放到你的项目根目录
>vcpkg
·>cd vcpkgM
·>./bootstrap-vcpkg.bat
·>./vcpkg integrate install
·>./vcpkg install fmt:x64-windows
·>cd ..
·>cmake-B build -DCMAKE_TOOLCHAIN_FILE="%CD%/vcpkg/scripts/buildsystems/vcpkg.cmake"
基于 TBB 的版本:任务组
用一个任务组 tbb::task_group 启动多个任务,一个负责下载,一个负责和用户交互。并在主线程中等待该任务组里的任务全部执行完毕。
区别在于,一个任务不一定对应一个线程,如果任务数量超过CPU最大的线程数,会由 TBB 在用户层负责调度任务运行在多个预先分配好的线程,而不是由操作系统负责调度线程运行在多个物理核心。
parallel_invoke
并行循环
时间复杂度(time-efficiency)与工作量复杂度(work-efficiency)
对于并行算法,复杂度的评估则要分为两种:
时间复杂度:程序所用的总时间(重点)
工作复杂度:程序所用的计算量(次要)
这两个指标都是越低越好。时间复杂度决定了快慢,工作复杂度决定了耗电量。
- 通常来说,工作复杂度 = 时间复杂度 * 核心数量
1个核心工作一小时,4个核心工作一小时。时间复杂度一样,而后者工作复杂度更高。
1个核心工作一小时,4个核心工作1/4小时。工作复杂度一样,而后者时间复杂度更低。
并行的主要目的是降低时间复杂度,工作复杂度通常是不变的。甚至有牺牲工作复杂度换取时间复杂度的情形。
并行算法的复杂度取决于数据量 n,还取决于线程数量 c,比如 O(n/c)。不过要注意如果线程数量超过了 CPU 核心数量,通常就无法再加速了,这就是为什么要买更多核的电脑。
也有一种说法,认为要用 c 趋向于无穷时的时间复杂度来衡量,比如 O(n/c) 应该变成 O(1)。
映射(map)
1个线程,独自处理8个元素的映射,花了8秒
用电量:1*8=8度电
结论:串行映射的时间复杂度为 O(n),工作复杂度为 O(n),其中 n 是元素个数
并行映射
4个线程,每人处理2个元素的映射,花了2秒
用电量:4*2=8度电
结论:并行映射的时间复杂度为 O(n/c),工作复杂度为 O(n),其中 c 是线程数量
parallel_for
基于迭代器区间:parallel_for_each
二维区间上的 for 循环:blocked_range2d
三维区间上的 for 循环:blocked_range3d
所有区间类型
缩并与扫描
缩并(reduce)
1个线程,依次处理8个元素的缩并,花了7秒
用电量:17=7度电
总用时:17=7秒
结论:串行缩并的时间复杂度为 O(n),工作复杂度为 O(n),其中 n 是元素个数
并行缩并
第一步、4个线程,每人处理2个元素的缩并,花了1秒
第二步、1个线程,独自处理4个元素的缩并,花了3秒
用电量:41+13=7度电
总用时:1+3=4秒
主要逻辑:把n个数的任务,分配个4个线程去做(用temp_res数组分别统计求和),按照每个线程处理[begin,end)之间的任务。最终将4个线程的值进行归并。
#include <iostream>
#include <tbb/task_group.h> // TBB 并行任务组
#include <vector>
#include <cmath> // 数学函数 sin()int main() {size_t n = 1<<26; // 2²⁶ = 67,108,864float res = 0; // 存储最终结果size_t maxt = 4; // 线程数tbb::task_group tg; // TBB 任务组std::vector<float> tmp_res(maxt); // 存储每个线程的局部结果
for (size_t t = 0; t < maxt; t++) {size_t beg = t * n / maxt; // 当前线程的起始索引size_t end = std::min(n, (t + 1) * n / maxt); // 结束索引tg.run([&, t, beg, end] { // 提交任务到线程池float local_res = 0;for (size_t i = beg; i < end; i++) {local_res += std::sin(i); // 计算局部和}tmp_res[t] = local_res; // 保存局部结果});
}分工逻辑:
4 个线程均分 n 个元素(每个线程处理约 16,777,216 个元素)。
tg.run() 将任务提交给 TBB 线程池异步执行。
tmp_res 存储每个线程的局部和。
合并局部结果
for (size_t t = 0; t < maxt; t++) {res += tmp_res[t]; // 汇总所有线程的结果
}
std::cout << res << std::endl;
结论:并行缩并的时间复杂度为 O(n/c+c),工作复杂度为 O(n),其中 n 是元素个数
类似加法分配律的思想,1+2+3+4,方法:((1+2)+3)+4或者(1+2)+(3+4)。类似这样的思想
性能测试
参考
- 【C++】transform, reduce, scan是什么意思?理解常用并行算法及其实现原理
- slide