本文档尝试快速介绍 OpenMP(如版本 4.5),这是一个简单的 C/C++/Fortran 编译器扩展 C++,它允许在现有源代码中添加并行性,而无需重写它。
引言 多线程的重要性 随着 CPU 速度不再像以前那样显著提高,多核系统正变得越来越流行。要利用这种功能,程序员在并行编程中知识化变得非常重要。本文档试图快速介绍 OpenMP,这是一个简单的 C/C++/Fortran 编译器扩展,允许将并行性添加到现有源代码中,而无需完全重写它。
多个编译器支持
GCC (GNU Compiler Collection)
Clang++
Solaris Studio
ICC (Intel C Compiler)
Microsoft Visual C++
C++中的OpenMP介绍 OpenMP 由一组编译器#pragmas
控制程序工作原理的编译器。pragmas
的设计使即使编译器不支持它们,程序仍将产生正确的行为,但没有任何并行性。 下面是演示 OpenMP 的两个简单的示例程序。你可以像这样编译它们:
Example: Initializing a table in parallel (multiple threads) 此代码将表初始化划分为多个线程,这些线程同时运行。每个线程初始化表的一部分
1 2 3 4 5 6 7 8 9 10 11 12 #include <cmath> int main() { const int size = 256; double sinTable[size]; #pragma omp parallel for for(int n=0; n<size; ++n) sinTable[n] = std::sin(2 * M_PI * n / size); // the table is now initialized }
Example: Initializing a table in parallel (single thread, SIMD) 此版本需要至少对 OpenMP 4.0 的编译器支持,并使用并行浮点库,如 AMD ACML 或英特尔SVML(可以在GCC中使用通过添加‑mveclibabi=svml)。
1 2 3 4 5 6 7 8 9 10 11 12 #include <cmath> int main() { const int size = 256; double sinTable[size]; #pragma omp simd for(int n=0; n<size; ++n) sinTable[n] = std::sin(2 * M_PI * n / size); // the table is now initialized }
Example: Initializing a table in parallel (multiple threads on another device) OpenMP 4.0 增加了对将代码卸载到不同设备(如 GPU)的支持。因此,单个程序中可以有三层并行性:单个线程处理多个数据;多个线程同时运行;和多个设备同时运行同一程序。
1 2 3 4 5 6 7 8 9 10 11 12 #include <cmath> int main() { const int size = 256; double sinTable[size]; #pragma omp target teams distribute parallel for map(from:sinTable[0:256]) for(int n=0; n<size; ++n) sinTable[n] = std::sin(2 * M_PI * n / size); // the table is now initialized }
综上所述,程序中很少指示它并行运行。如果删除 #pragma 行,则结果仍然是运行并执行预期操作。#pragma的有效 C++ 程序。它确实同时计算 N 个值,其中 N 是线程数。在 GCC 中,libgomp 从处理器数目确定这个N的值。 在C 和 C++ 标准中,如果编译器遇到#pragma但它不支持,它将忽略它。因此,添加 OMP 语句可以安全地完成,而不会破坏与旧编译器的兼容性。 还有一个运行时库,可以通过 omp.h 访问,但它不太经常需要。如果需要,可以在不支持 OpenMP #define _OPENMP检查条件编译的格式。
OpenMP语法解析 C 和 C++的所有 OpenMP 构造都用 #pragma,后跟参数,以新行结尾。#pragma通常仅适用于紧接着它的语句,但barrier
和flush
命令除外,它们没有关联的语句。
The parallel construct parallel构造用parallel关键字启动并行块。它创建一组N个数目线程(其中 N 在运行时确定,通常来自 CPU 内核的数量,但可能受到一些因素的影响),所有这些线程都执行下一个语句(或下一个块,如果语句是 […] - 存储模块)。语句之后,线程将重新联接到一个线程中。
1 2 3 4 5 #pragma omp parallel { // Code inside this region runs in parallel. printf("Hello!\n"); }
在内部,GCC 通过创建一个magic函数并移动关联的代码到该函数中实现此功能,使该块内声明的所有变量成为该函数的局部变量(从而成为每个线程的局部变量)。另一方面,ICC 使用类似于fork()的机制,并且不创建一个magic函数。当然,这两个实现都是有效和语义上相同的。 从上下文共享的变量以透明方式处理,有时通过传递引用,有时通过使用在并行块末尾刷新的寄存器变量(或每当执行flush
时)。
Parallelism conditionality clause: if 通过在并行命令中包括 if 子句,可以使并行性成为条件,例如:
1 2 3 4 extern int parallelism_enabled; #pragma omp parallel for if(parallelism_enabled) for(int c=0; c<n; ++c) handle(c);
在这种情况下,如果parallelism_enabled值为零,则for循环中开启的线程数将始终为一个。
Loop construct: for for
关键字把for-loop
拆分,因此每个线程都可以单独处理在循环里面不同的部分。
1 2 3 4 5 6 #pragma omp for for(int n=0; n<10; ++n) { printf(" %d", n); } printf(".\n");
此循环将输出 0…9 一次。但是,它可以按任意顺序执行。它可以输出,例如
事实上,上述代码和如下代码效果等同:
1 2 3 4 5 int this_thread = omp_get_thread_num(), num_threads = omp_get_num_threads(); int my_start = (this_thread ) * 10 / num_threads; int my_end = (this_thread+1) * 10 / num_threads; for(int n=my_start; n<my_end; ++n) printf(" %d", n);
因此每个线程都会得到不同的部分代码,并且它们并行的执行只属于自己的部分。
也可以显示的指定线程数目,通过使用关键字num_threads
。
1 2 3 4 5 6 7 8 9 #pragma omp parallel num_threads(3) { // This code will be executed by three threads. // Chunks of this loop will be divided amongst // the (three) threads of the current team. #pragma omp for for(int n=0; n<10; ++n) printf(" %d", n); }
注意OpenMP同样对C语言适用。在C里面,你需要显示的指定循环变量使用关键字private
,因为C不允许在for循环体内声明变量。
1 2 3 4 int n; #pragma omp for private(n) for(n=0; n<10; ++n) printf(" %d", n); printf(".\n");
parallel, for and a team概念
parallel for :parallel for是两个命令的组合,parallel和for 。:parallel 创建一个新team,for为该team拆分以处理循环的不同部分。
for:用于在当前team的线程之间划分 for 循环的工作。它不创建线程,它只将工作划分到当前正在执行团队的线程之间。
team: 是当前执行所有线程的组合。在程序开始的时候,team里面只包含一个线程。parallel关键字把当前线程切分为多个线程team,直到执行完毕
调度策略 for-loop 的调度算法可以显式控制。
1 2 3 #pragma omp for schedule(static) for(int n=0; n<10; ++n) printf(" %d", n); printf(".\n")
共有5种调度算法:static
, dynamic
, guided
, auto
, runtime
(OpenMP 4.0之后)。之外在OpenMP4.5之后,又添加了新的3种monotonic
, nonmonotonic
, simd
。
使用如下demo验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <omp.h> #include <iostream> #define COUNT 48 int main() { #pragma omp parallel for schedule(static) /* #pragma omp parallel for schedule(static, 2) */ for(int i = 0;i < COUNT; i++) { printf("Thread: %d, Iteration: %d\n", omp_get_thread_num(), i); } return 0; }
Build and Run:
1 2 $ g++ omp.cpp -fopenmp $ ./a.out
本机配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 (base) huang@mlp:~/taichi/omp$ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz Stepping: 10 CPU MHz: 3907.882 CPU max MHz: 4700.0000 CPU min MHz: 800.0000 BogoMIPS: 7399.70 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 12288K NUMA node0 CPU(s): 0-11
static static
是默认的调度算法。在上面的所有例子中,每个线程独立的决定它们处理哪个loop片段。schedule(static, chunk-size)
子句指定 for 循环具有静态调度类型。OpenMP 将迭代划分为大小块大小的块,并将区块按循环顺序分发到线程。当chunk-size
没有指定的时候OpenMP将迭代划分为大小大致相等的区块,并且最多将一个区块分发到每个线程。 下面是关于static
调度的例子。
1 2 3 4 5 schedule(static): **************** **************** **************** ****************
实际效果如下:这个分配是静态的,“静态”体现在这个分配过程跟实际的运行是无关的,可以从逻辑上推断出哪几次迭代会在哪几个线程上运行。具体而言,对于一个N次迭代,使用M个线程,那么,[0,size-1]的size次的迭代是在第一个线程上运行,[size, size + size -1]是在第二个线程上运行,依次类推。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 (base) huang@mlp:~/omp$ ./a.out Thread: 11, Iteration: 44 Thread: 11, Iteration: 45 Thread: 11, Iteration: 46 Thread: 5, Iteration: 20 Thread: 5, Iteration: 21 Thread: 5, Iteration: 22 Thread: 5, Iteration: 23 Thread: 1, Iteration: 4 Thread: 1, Iteration: 5 Thread: 1, Iteration: 6 Thread: 1, Iteration: 7 Thread: 10, Iteration: 40 Thread: 10, Iteration: 41 Thread: 10, Iteration: 42 Thread: 10, Iteration: 43 Thread: 7, Iteration: 28 Thread: 7, Iteration: 29 Thread: 7, Iteration: 30 Thread: 7, Iteration: 31 Thread: 8, Iteration: 32 Thread: 8, Iteration: 33 Thread: 8, Iteration: 34 Thread: 2, Iteration: 8 Thread: 11, Iteration: 47 Thread: 6, Iteration: 24 Thread: 6, Iteration: 25 Thread: 6, Iteration: 26 Thread: 6, Iteration: 27 Thread: 4, Iteration: 16 Thread: 4, Iteration: 17 Thread: 4, Iteration: 18 Thread: 4, Iteration: 19 Thread: 2, Iteration: 9 Thread: 2, Iteration: 10 Thread: 2, Iteration: 11 Thread: 9, Iteration: 36 Thread: 9, Iteration: 37 Thread: 9, Iteration: 38 Thread: 9, Iteration: 39 Thread: 8, Iteration: 35 Thread: 0, Iteration: 0 Thread: 0, Iteration: 1 Thread: 0, Iteration: 2 Thread: 0, Iteration: 3 Thread: 3, Iteration: 12 Thread: 3, Iteration: 13 Thread: 3, Iteration: 14 Thread: 3, Iteration: 15
1 2 3 4 5 schedule(static, 4): **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
1 2 3 4 5 schedule(static, 8): ******** ******** ******** ******** ******** ******** ******** ********
让我解释一下这些例子。我们使用 64 次迭代并行化 for 循环,并且使用四个线程并行化 for 循环。示例中的每一行星数表示一个线程。每列表示迭代。当所有迭代具有相同的计算成本时,静态调度类型是适当的。
dynamic schedule(dynamic, chunk-size)
子句指定 for 循环具有动态调度类型。OpenMP 将迭代划分为大小块大小的块。每个线程执行一个迭代区块,然后请求另一个区块,直到不再有可用的区块。没有将区块以特定顺序分布到线程。每次执行 for 循环时,顺序会更改。 如果没有指定chunk-size
,默认为1。下面是一些动态调度的例子。
1 2 3 4 5 schedule(dynamic): * ** ** * * * * * * ** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ** * * * * *
对于dynamic,没有size参数的情况下,每个线程按先执行完先分配的方式执行1次循环。实际运行情况如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Thread: 5, Iteration: 4 Thread: 5, Iteration: 12 Thread: 5, Iteration: 13 Thread: 5, Iteration: 14 Thread: 5, Iteration: 15 Thread: 5, Iteration: 16 Thread: 5, Iteration: 17 Thread: 5, Iteration: 18 Thread: 5, Iteration: 19 Thread: 5, Iteration: 20 Thread: 5, Iteration: 21 Thread: 5, Iteration: 22 Thread: 5, Iteration: 23 Thread: 5, Iteration: 24 Thread: 5, Iteration: 25 Thread: 5, Iteration: 26 Thread: 5, Iteration: 27 Thread: 5, Iteration: 28 Thread: 5, Iteration: 29 Thread: 5, Iteration: 30 Thread: 5, Iteration: 31 Thread: 5, Iteration: 32 Thread: 5, Iteration: 33 Thread: 5, Iteration: 34 Thread: 5, Iteration: 35 Thread: 5, Iteration: 36 Thread: 5, Iteration: 37 Thread: 5, Iteration: 38 Thread: 5, Iteration: 39 Thread: 5, Iteration: 40 Thread: 5, Iteration: 41 Thread: 5, Iteration: 42 Thread: 5, Iteration: 43 Thread: 5, Iteration: 44 Thread: 5, Iteration: 45 Thread: 5, Iteration: 46 Thread: 5, Iteration: 47 Thread: 9, Iteration: 1 Thread: 0, Iteration: 6 Thread: 3, Iteration: 2 Thread: 7, Iteration: 0 Thread: 6, Iteration: 5 Thread: 8, Iteration: 3 Thread: 10, Iteration: 8 Thread: 1, Iteration: 7 Thread: 11, Iteration: 9 Thread: 4, Iteration: 10 Thread: 2, Iteration: 11
1 2 3 4 5 schedule(dynamic, 1): * * * * * * * * * * * * * * * * * * * * * * * * * * *** * * * * * * * * ** * * * * * * * * * * * * * ** * * * * * * * * * *
1 2 3 4 5 schedule(dynamic, 4): **** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
实际运行情况如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 (base) huang@mlp:~/taichi/omp$ ./a.out Thread: 0, Iteration: 12 Thread: 0, Iteration: 13 Thread: 0, Iteration: 14 Thread: 0, Iteration: 15 Thread: 3, Iteration: 28 Thread: 3, Iteration: 29 Thread: 3, Iteration: 30 Thread: 3, Iteration: 31 Thread: 8, Iteration: 8 Thread: 8, Iteration: 9 Thread: 8, Iteration: 10 Thread: 8, Iteration: 11 Thread: 4, Iteration: 4 Thread: 4, Iteration: 5 Thread: 4, Iteration: 6 Thread: 4, Iteration: 7 Thread: 11, Iteration: 36 Thread: 11, Iteration: 37 Thread: 11, Iteration: 38 Thread: 11, Iteration: 39 Thread: 5, Iteration: 24 Thread: 5, Iteration: 25 Thread: 7, Iteration: 32 Thread: 2, Iteration: 44 Thread: 9, Iteration: 16 Thread: 10, Iteration: 0 Thread: 10, Iteration: 1 Thread: 10, Iteration: 2 Thread: 10, Iteration: 3 Thread: 7, Iteration: 33 Thread: 7, Iteration: 34 Thread: 7, Iteration: 35 Thread: 9, Iteration: 17 Thread: 9, Iteration: 18 Thread: 9, Iteration: 19 Thread: 6, Iteration: 20 Thread: 6, Iteration: 21 Thread: 6, Iteration: 22 Thread: 6, Iteration: 23 Thread: 5, Iteration: 26 Thread: 5, Iteration: 27 Thread: 2, Iteration: 45 Thread: 2, Iteration: 46 Thread: 2, Iteration: 47 Thread: 1, Iteration: 40 Thread: 1, Iteration: 41 Thread: 1, Iteration: 42 Thread: 1, Iteration: 43
1 2 3 4 5 schedule(dynamic, 8): ******** ******** ******** ******** ******** ******** ******** ********
当迭代需要不同的计算成本时,动态调度类型是合适的。这意味着迭代之间的平衡很差。动态计划类型比静态计划类型具有更高的开销,因为它在运行时动态分布迭代。
Guided Guided
调度算法类似于动态计划类型。OpenMP 再次将迭代划分为块。每个线程执行一个迭代块,然后请求另一个区块,直到不再有可用的区块。与动态调度类型的区别在于chunk-size
。区块的大小与未分配迭代数除以线程数成正比。因此,区块的大小减小。 区块的最小大小由chunk-size
设置。我们在调度确定它:schedule(guided, chunk-size)
。但是,包含上次迭代的区块的大小可能小于chunk-size
。 如果我们不指定chunk-size
,则默认为 1。对应示例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 schedule(guided): ********* * ************ ******* *** ******* * **************** ***** ** * schedule(guided, 2): ************ **** ** ******* *** ** ********* **************** ***** ** ** schedule(guided, 4): ******* ************ **** **** ********* **************** ***** **** *** schedule(guided, 8): ************ ******** *** **************** ******** ********* ********
我们可以看出chunk的大小一直在递减。第一个chunk总是有16个iteration,这是因为我们有64个iteration共有4个线程去并行这个loop。 我们还可以看到,最小区块大小在计划子句中确定。唯一的例外是最后一个区块。其大小可能低于规定的最小大小。
当迭代之间不平衡时,引导调度类型是合适的。初始区块较大,因为它们减少了开销。较小的区块在计算结束时填充计划并改善负载平衡。当在计算接近尾声时发生负载平衡不佳的情况时,此调度类型尤其合适。
Auto Auto
调度算法将调度决策委托给编译器和runtime。在下面的示例中,编译器/系统确定静态调度。
1 2 3 4 5 schedule(auto): **************** **************** **************** ****************
Runtime Runtime
调度策略将有关计划的决定推迟到运行时。本例中,我们已经描述了指定计划类型的不同方法。一个选项是环境变量OMP_SCHEDULE
,另一个选项是函数omp_set_schedule
。
Default 如果我们不指定任何调度策略:
1 2 3 #pragma omp parallel for for (...) { ... }
OpenMP
使用默认的调度策略,由内部的控制变量def-sched-var
决定。在我的机器上面结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Thread: 5, Iteration: 20 Thread: 5, Iteration: 21 Thread: 5, Iteration: 22 Thread: 5, Iteration: 23 Thread: 1, Iteration: 4 Thread: 1, Iteration: 5 Thread: 1, Iteration: 6 Thread: 1, Iteration: 7 Thread: 0, Iteration: 0 Thread: 0, Iteration: 1 Thread: 0, Iteration: 2 Thread: 0, Iteration: 3 Thread: 6, Iteration: 24 Thread: 6, Iteration: 25 Thread: 6, Iteration: 26 Thread: 6, Iteration: 27 Thread: 9, Iteration: 36 Thread: 9, Iteration: 37 Thread: 9, Iteration: 38 Thread: 9, Iteration: 39 Thread: 10, Iteration: 40 Thread: 4, Iteration: 16 Thread: 4, Iteration: 17 Thread: 4, Iteration: 18 Thread: 4, Iteration: 19 Thread: 2, Iteration: 8 Thread: 2, Iteration: 9 Thread: 2, Iteration: 10 Thread: 2, Iteration: 11 Thread: 8, Iteration: 32 Thread: 8, Iteration: 33 Thread: 8, Iteration: 34 Thread: 8, Iteration: 35 Thread: 3, Iteration: 12 Thread: 3, Iteration: 13 Thread: 3, Iteration: 14 Thread: 3, Iteration: 15 Thread: 7, Iteration: 28 Thread: 7, Iteration: 29 Thread: 7, Iteration: 30 Thread: 7, Iteration: 31 Thread: 10, Iteration: 41 Thread: 10, Iteration: 42 Thread: 10, Iteration: 43 Thread: 11, Iteration: 44 Thread: 11, Iteration: 45 Thread: 11, Iteration: 46 Thread: 11, Iteration: 47
参考:https://bisqwit.iki.fi/story/howto/openmp/ https://www.openmp.org/wp-content/uploads/omp-hands-on-SC08.pdf https://stackoverflow.com/questions/1448318/omp-parallel-vs-omp-parallel-for http://jakascorner.com/blog/2016/06/omp-for-scheduling.html