TBB学习笔记五([Algorithms.parallel_for])
《Today’s TBB 2nd Edition》
parallel_for - Independent Tasks from a Known Set of Loop Iterations
tbb::parallel_for
是一个函数模板,将for-loop的迭代视作独立任务。一些函数签名展示在下面。
//! Parallel iteration over a range of integers with a default step value, explicit task group context, and simple partitioner
template <typename Index, typename Function>
__TBB_requires(parallel_for_index<Index> && parallel_for_function<Function, Index>)
void parallel_for(Index first, Index last, const Function& f, const simple_partitioner& partitioner, task_group_context &context) {
parallel_for_impl<Index,Function,const simple_partitioner>(first, last, static_cast<Index>(1), f, partitioner, context);
}
__TBB_requires(...)
是C++20下编译时静态断言,用于检查模板参数是否满足tbb::parallel_for
的概念(Concept)。①验证index是否满足并行循环索引需求;②验证Function是否符合并行任务的接口规范(比如可调用对象的需求、const修饰的operator()…);- 简单分区器(simple_partitioner):递归划分迭代范围;
- 任务组上下文(task_group_context):管理并行任务的执行状态;
- 默认步长:
static_cast<Index>(1)
硬编码步长,以步长 1 1 1迭代。
类似的如下的函数签名,可以显式的去指定步长。
//! Parallel iteration over a range of integers with explicit step, task group context, and simple partitioner
template <typename Index, typename Function>
__TBB_requires(parallel_for_index<Index> && parallel_for_function<Function, Index>)
void parallel_for(Index first, Index last, Index step, const Function& f, const simple_partitioner& partitioner, task_group_context &context) {
parallel_for_impl<Index,Function,const simple_partitioner>(first, last, step, f, partitioner, context);
}
下面的是另一个重载的函数签名版本。
//! Parallel iteration over range with simple partitioner and user-supplied context.
/** @ingroup algorithms **/
template<typename Range, typename Body>
__TBB_requires(tbb_range<Range> && parallel_for_body<Body, Range>)
void parallel_for( const Range& range, const Body& body, const simple_partitioner& partitioner, task_group_context& context ) {
start_for<Range,Body,const simple_partitioner>::run(range, body, partitioner, context);
}
- tbb_range<Range>检查Range类型是否满足Range概念以及parallel_for_body<Body, Range>则检验是否满足Body概念;
- range:迭代范围(如 blocked_range),定义并行处理的区间;
- body: 处理每个子范围的逻辑;
- simple_partitioner: 分区策略,控制任务划分粒度;
在tbb\parallel_for.h
中,有着相当多的重载的tbb::parallel_for
的函数签名,所有的函数签名共有之处是可以通过递归地细分范围来有效地生成任务。tbb::parallel_for
对循环结构的天然适配性和高效的并行能力,已经成为了最广泛使用的工具。其相较于 parallel_invoke 在单层分叉-合并(fork-join)并行性中的扩展性优势。比如专为迭代密集型:数据的遍历、矩阵计算。
- 例子①:
for(int i = 0; i < N; ++i) {
a[i] = f(a[i]);
}
//use a parallel_for to make this loop parallel
tbb::parallel_for(0, N, [&](int i) {a[i] = f(a[i]));});
当然,tbb::parallel_for
的核心假设迭代是独立的,且执行顺序不影响最终结果。独立计算的场景像
s
u
m
+
=
a
[
i
]
2
sum += a[i]^2
sum+=a[i]2,没有任何问题,而这个场景
a
[
i
]
=
a
[
i
−
1
]
∗
2
a[i] = a[i-1] * 2
a[i]=a[i−1]∗2,并行化时执行顺序会改变
a
\mathbf{a}
a向量存储的值,我们可以手动划分依赖链或者利用其他的算法tbb::parallel_reduce
。
template<typename InMat1, typename InMat2, typename OutMat>
void simpleSerialMatrixProduct(int M, int N, int K, const InMat1& a, const InMat2& b, OutMat& c) {
for (int i0 = 0; i0 < M; ++i0) {
for (int i1 = 0; i1 < N; ++i1) {
auto& c0 = c[i0*N+i1];
for (int i2 = 0; i2 < K; ++i2) {
c0 += a[i0*K+i2] * b[i2*N + i1];
}
}
};
}
这个函数实现了两个矩阵
a
M
∗
K
\mathbf{a_{M*K}}
aM∗K和
b
K
∗
N
\mathbf{b_{K*N}}
bK∗N的行主序(Row-Major Order)乘法,结果存在
c
M
∗
N
\mathbf{c_{M*N}}
cM∗N。我们可以立即使用tbb::parallel_for
来实现并行化。
template<typename InMat1, typename InMat2, typename OutMat>
void simpleParallelMatrixProduct(int M, int N, int K, const InMat1& a, const InMat2& b, OutMat& c) {
tbb::parallel_for( 0, M, [&](int i0) {
for (int i1 = 0; i1 < N; ++i1) {
double& c0 = c[i0*N+i1];
for (int i2 = 0; i2 < K; ++i2) {
c0 += a[i0*K+i2] * b[i2*N + i1];
}
}
});
}
并行策略就是外层循环
i
0
i_0
i0(行索引)被并行化,每个线程独立处理一行或多行。当然这边有潜在的问题,比如处理一行时,粒度可能比较大。后续对tbb::parallel_for
结合其他算法模块的优化进行讨论。
总之,tbb::parallel_for
是最常用的一个并行算法,简洁且多样的API设计能够适配多种数据结构,灵活性和高并行性能可结合多种模式算法,所谓百样玲珑。