导语

OpenMP(Open Multi-Processing)作为一种共享内存的并行编程模型[1],是目前国际上最主流的异构混合并行编程架构之一,自1997年开始推出至今已升级5个主要版本,最新的标准规范为OpenMP API 5.2 [2]。OpenMP API定义了一个可移植、易扩展的编程模型,提供了一系列简单而又灵活的接口(指导语句)用于指导用户简化应用代码和提升性能。OpenMP普遍应用于科学计算领域,如理论物理、化学、材料、气象等领域的数值模拟计算。

毕昇编译器作为一款支持异构并行计算的高性能编译器,支持C/C++/Fortran语言的OpenMP API。目前毕昇编译器基本支持C/C++ OpenMP API 5.0 [3]和Fortran OpenMP API 4.5 [4],并持续提升支持最新标准规范。本文主要讲解毕昇编译器如何使用OpenMP API以及比较常用的指导语句。

使用OpenMP API范例

这里给出一个简单的Fortran代码(test.f90),开启4个线程,并行打印每个线程的信息。

1  program main
 2  use omp_lib
 3  integer :: id, num_threads
 4
 5  call omp_set_num_threads(4) !设置默认创建线程数为4
 6  !$omp parallel private(id) !创建4个线程
 7  id = omp_get_thread_num() !获取当前线程ID
 8  num_threads = omp_get_num_threads() !获取当前创建的线程总数
 9  print *, "Hello World Id ", id, " of ", num_threads, " num_threads"
10  !$omp end parallel !结束parallel指导语句,退出并行区域
11  end

使用毕昇编译器进行编译和运行,结果如下所示:

1  $ flang test.f90 -fopenmp -o a.out
2
3  $ ./a.out
4
5  Hello World Id 3 of 4 num_threads
6  Hello World Id 0 of 4 num_threads
7  Hello World Id 1 of 4 num_threads
8  Hello World Id 2 of 4 num_threads

从结果可以看出,该用例创建了4个线程,并行执行7-9行代码,故print打印语句执行了4次,每个线程打印对应线程的ID。

OpenMP常用指导语句

1. parallel指令

parallel指令是最常用的OpenMP API之一,作用是创建一个线程组,parallel指令是进行并行控制的关键。根据OpenMP API标准规范,每个指导语句可以跟随一些子句,用于指导更加详细的功能,比如parallel指令可以跟随if,num_threads,private,firstprivate,shared,default,copyin,reduction,proc_bind,allocate子句。常用子句的详细使用指导如下:

(1)if子句:if ([parallel:] scalar-logical-expression),如果scalar-logical-expression的结果为真,则创建一个线程组,含多个线程;如果结果为假,则只创建一个线程。

(2)num_threads子句:num_threads (scalar-integer-expression),在parallel并行区域内创建多个线程,线程数为scalar-integer-expression。

(3)private子句:private(list),list为一系列变量,list所有变量在并行区域内为私有变量,即每个线程有一份私有拷贝,其它线程不可访问。

(4)firstprivate子句:firstprivate(list),private子句的升级版,除了定义list所有变量在并行区域内为私有变量,还将进入parallel并行区域前的变量的值赋值给每个线程对应的私有变量。

(5)shared子句:shared(list),list所有变量共享内存,所有线程均可访问,并不进行拷贝,与并行区域外的同名变量为同一个变量。

这里需要注意在并行区域结束处有一个隐含的同步操作,即所有线程都到达并行区域结束处,主线程才会继续执行并行区域外的代码。

2. worksharing-loop指令

worksharing-loop指令是最常用的OpenMP API之一,它的作用是对循环结构进行并行处理。C/C++的指导语句为#pragma omp for [子句],Fortran的指导语句为!$omp do。worksharing-loop指令指导接下来的循环被并行执行,子句指导如何被并行执行。worksharing-loop指令可以跟随private,firstprivate,lastprivate,schedule,collapse,nowait,linear,reduction,ordered,allocate。常用子句的详细使用指导如下:

(1)private子句:private(list),用法同parallel的private子句,区别为list变量的范围为循环区域。

(2)firstprivate子句:firstprivate(list),用法同parallel的private子句,区别为list变量的范围为循环区域。

(3)lastprivate子句:lastprivate([lastprivate-modifier:] list),private子句的升级版,除了定义list所有变量在并行区域内为私有变量,还将循环最后一次迭代的子线程的私有变量的值赋值给并行区域外同名的原始变量。

(4)schedule子句:schedule([modifier[, modifier]:]kind[, chunk_size]),指导循环的迭代如何调度到创建的线程上,具体调度算法或将在后续文章中详述。

(5)collapse子句:collapse(n),将指令下的前n层循环进行合并后展开为一个更大的循环,增加可以调度的循环总数。

(6)nowait子句:nowait,在指令结束处不生成隐含的同步操作。如果没有该子句,在指令结束处将生成隐含的同步操作。

parallel和worksharing-loop指令的使用范例

1  program main
 2    use omp_lib
 3    integer, parameter :: N = 10
 4    integer :: i, tid, a(N), b(N)
 5
 6    do i = 1, N
 7      a(i) = -1
 8      b(i) = -1
 9    enddo
10
11    !$omp parallel num_threads(N)
12    !$omp do private(tid) firstprivate(a, b) lastprivate(b)
13    do i = 1, N
14      tid = omp_get_thread_num()
15      a(i) = a(i) + i + tid
16      b(i) = b(i) + i + tid
17      if (i == 1 .or. i == N) print *, i, tid, a(i), b(i)
18    enddo
19    !$omp end do
20    !$omp end parallel
21
22    print *, a
23    print *, b
24  end

该用例与下面的用例等价:

1  program main
 2    use omp_lib
 3    integer, parameter :: N = 10
 4    integer :: i, tid, a(N), b(N)
 5
 6    do i = 1, N
 7      a(i) = -1
 8      b(i) = -1
 9    enddo
10
11    !$omp parallel do private(tid) firstprivate(a, b) lastprivate(b) num_threads(N)
12    do i = 1, N
13      tid = omp_get_thread_num()
14      a(i) = a(i) + i + tid
15      b(i) = b(i) + i + tid
16      if (i == 1 .or. i == N) print *, i, tid, a(i), b(i)
17    enddo
18    !$omp end parallel do
19
20    print *, a
21    print *, b
22  end

使用毕昇编译器进行编译和运行,结果如下所示:

1  $ flang test.f90 -fopenmp -o a.out
2
3  $ ./a.out
4
5  1 0 0 0
6  10 9 18 18
7  -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
8  -1 -1 -1 -1 -1 -1 -1 -1 -1 18

OpenMP指导语句使得该程序将a和b数组在循环内进行私有拷贝,并且将并行区域外的同名变量(a, b)的值(-1)拷贝进并行区域内,计算之后,最后一次迭代的b数组的值(b(10)=18)赋给并行区域外,除此之外,并未改变并行区域外的同名变量(a, b)的值。

参考

[1] https://www.openmp.org/

[2] https://www.openmp.org/wp-content/uploads/OpenMP-API-Specification-5-2.pdf

[3] https://www.openmp.org/spec-html/5.0/openmp.html

[4] https://www.openmp.org/wp-content/uploads/openmp-4.5.pdf