本文例子处代码块由于行宽不够,可能会对读者造成不便,可以将文本内容复制到notepad或者是visual studio code中查看,敬请谅解。
OpenMP的特长就在于并行循环。我们知道下面这段代码可以启用并行:
#pragma omp parallel for
for ( ... )
{ ... }
OpenMP对于并行化循环,会创建一个线程池,然后把for循环里的运算分配给线程。这里的运算是指一轮for循环(循环下标+1)。
Scheduling
接下来我们来讨论一下程序员如何决定一个循环的scheduling类型。
Explicit
如果有一个显式schedule
子句:
#pragma omp parallel for schedule(scheduling-type)
for (...)
{ ... }
那么OpenMP就会使用scheduling-type
来设定for loop的并行方式。
scheduling-type包括:
static
dynamic
guided
auto
runtime
我们会在后面详细讨论。
Runtime
如果scheduling-type
(在宏定义里)是runtime
,那么OpenMP将会调用系统设定的run-sched-var
来决定schedule类型。设定OpenMP中的run-sched-var
由两种方法:
- 我们可以通过设定环境变量
OMP_SCHEDULE
来设定run-sched-var
。
例如我们在bash-like的终端里输入
$ export OMP_SCHEDULE=scheduling-type
来设定环境变量OMP_SCHEDULE
。
- 另外我们也可以在自己程序里调用函数设定:
...
omp_set_schedule(scheduling-type);
...
Default
如果没有显式指定schedule子句
#pragma omp parallel for
for (...)
{ ... }
那么OpenMP使用Default
scheduling模式。具体使用的类型将由OpemMP内部变量def-shed-var
决定
The scheduling types
正如前面提到过的,一共有五种scheduing类型
scheduling-type包括:
static
dynamic
guided
auto
runtime
Static
schedule(static, chunk-size)
子句指定了for循环使用static
schedule模式。OpenMP把for的总共次数划分成一个个小块(chunks),chunk-size
规定了每个线程一次执行多少次for循环。
如果没有指定chunk-size
,OpenMP将会把for的迭代分成近乎相同的块,然后尽量给每个线程分配连续的一次运算。
举个例子:
schedule(static): --no chunk-size parameter **************** **************** **************** ****************
schedule(static, 4): **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
schedule(static, 8): ******** ******** ******** ******** ******** ******** ******** ********
对于这个例子,简单来说我们想要并行一个64次的循环,我们有4个线程。在上面的代码块里,每一行代表一个线程,每个*富豪代表一次循环。
在第一个代码块(对应schedule(static)
)里,第一个线程运行了16次循环,假设for循环里的循环变量为i,这16次循环对应了i=0,1,2,...15
,同理,第二个线程运行了i=16,17,18,19...31
其他两个以此类推,每个线程执行的分别是四个连续的循环和8个连续的循环,类似i=0,1,2,3,8,9,10,11...
如果每个循环都有相同的计算时间的话,换言之,如果循环的计算和循环变量的增加没关系的话,使用static可以减小额外开销(overhead)。
Dynamic
schedule(dynamic, chunk-size)
子句指定for loop有动态的scheduling type,OpenMP将会根据chunk-size
划分运算块。每个块先运行OpenMP给分配好的一个chunk,运行完以后OpenMP将再次分配。直到没有剩余块
这意味着在运行程序前,我们无法预知线程和某次循环的对应关系,每次运行线程执行的顺序都会改变。
chunk-size默认为1.
例子:
schedule(dynamic): * ** ** * * * * * * ** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ** * * * * *
schedule(dynamic, 1): * * * * * * * * * * * * * * * * * * * * * * * * * * *** * * * * * * * * ** * * * * * * * * * * * * * ** * * * * * * * * * *
schedule(dynamic, 4): **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
schedule(dynamic, 8): ******** ******** ******** ******** ******** ******** ******** ********
可以发现前两种schedule是一致的。后面两种OpenMP分别给每个线程4或者8个大小的块,每个线程运行循环数量不一定相同。
动态scheduling type适用于在已知不同的iteration会有不同的时间消耗(cost)。交给OpenMP来处理负载可能比人手动会高效一些。但是动态分配的过程会显著提升overhead.
Guided
guided
和dynamic
非常相似。对于guided
, OpenMP依然是将多次循环分成一个个块,每个线程执行块,执行结束后再次向OpenMP索要新块,直到没有新块。
guided和dynamic的区别在于块的大小,dynamic里块的大小是固定的,guided的块的大小会调整为剩余迭代的次数和线程数之比。随着程序的运行,块的大小会越来越小。
我们可以使用chunk-size
参数来规定guided
最小块的大小。但是,最后一次迭代可能会小于chunk-size.
chunk-size默认为1
schedule(guided, 4): ******* ************ **** **** ********* **************** ***** **** ***
schedule(guided, 2): ************ **** ** ******* *** ** ********* **************** ***** ** **
schedule(guided): ********* * ************ ******* *** ******* * **************** ***** ** *
schedule(guided, 8): ************ ******** *** **************** ******** ********* ********
这里一共有64次迭代,最开始,系统给每个线程分配16次迭代(16=64/4,64次迭代,4个线程),之后再分配是12(12=(64-16)/4)次,依次类推。
Guided适用于每次迭代时间消耗不同的情况。并且由于最开始块很大,所以会减少overhead。
Auto
这种情况下OpenMP自动给定scheduling类型。