在 OpenMP 并行区域中声明变量的作用域属性称为确定作用域。通常,如果将变量的作用域确定为 SHARED,则所有线程共享该变量的一个副本。如果将变量的作用域确定为 PRIVATE,则每个线程各有一份该变量副本。OpenMP 拥有丰富的数据环境。除了 SHARED 和 PRIVATE 之外,还可以将变量的作用域声明为 FIRSTPRIVATE、LASTPRIVATE、REDUCTION 或 THREADPRIVATE。
OpenMP 要求用户声明在并行区域中使用的每个变量的作用域。这是一个单调乏味、容易出错的过程,并且公认是使用 OpenMP 将程序并行化的过程中最艰难的部分。
Sun Studio C、C++ 和 Fortran 95 编译器提供了自动确定作用域的功能。这些编译器可以分析并行区域的执行和同步模式,并基于一组作用域确定规则自动确定变量的作用域。
自动确定数据作用域子句是 Sun 对 OpenMP 规范的扩展。通过使用以下两种子句之一,用户可以指定要自动确定作用域的变量。
语法:
并行构造中的 __auto 子句可指示编译器自动确定构造中已命名变量的作用域。(请注意 auto 前面的两个下划线)。
__auto 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS 中或 Fortran 95 的 PARALLEL WORKSHARE 指令中。
如果在 __auto 子句中指定了变量,则不能在其他任何数据作用域子句中指定该变量。
并行构造中的 default(__auto) 子句可指示编译器自动确定构造中引用的所有未在任何数据作用域子句中显式确定作用域的变量的作用域。
default(__auto) 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS 中或 Fortran 95 的 PARALLEL WORKSHARE 指令中。
在自动确定作用域的情况下,编译器应用以下规则来确定并行区域中变量的作用域。
这些规则并不适用于由 OpenMP 规范隐式确定作用域的变量,如工作共享 DO 循环或 FOR 循环的循环索引变量。
S1:对于组中执行并行区域的线程而言,如果在该区域中使用变量不会导致数据争用情形,则将变量的作用域确定为 SHARED。
S2:如果在每个执行并行区域的线程中,变量总是在被同一线程读取之前被写入,则将变量的作用域确定为 PRIVATE。如果可以将变量的作用域确定为 PRIVATE,并且该变量在写入(在并行区域之后写入)之前被读取,而构造为 PARALLEL DO 或 PARALLEL SECTIONS,则将其作用域确定为 LASTPRIVATE。
S3:如果在编译器可以识别的约简操作中使用变量,则将该变量的作用域确定为具有该特定操作类型的 REDUCTION。
A1:对于组中执行并行区域的线程而言,如果在该区域中使用数组不会导致数据争用情形,则将数组的作用域确定为 SHARED。
在自动确定没有隐式作用域的变量的作用域时,编译器将根据上述规则按给定顺序 S1–S3(如果变量为标量)以及上述规则 A1(如果变量为数组)检查变量的使用情况。如果符合某个规则,编译器将按照匹配的规则确定变量的作用域。如果不符合某个规则,编译器将尝试使用下一个规则。如果编译器找不到匹配项,它将放弃确定该变量的作用域的尝试,并将该变量的作用域确定为 SHARED,然后像指定了 IF (.FALSE.) 或 if(0) 子句一样,将绑定并行区域序列化。
作用域的自动确定失败的原因有两个。一个原因是使用的变量不匹配任何规则。第二个原因是源代码对于编译器来说过于复杂,因而无法执行全面的分析。函数调用、复杂的数组下标、内存别名和用户实现的同步都是常见原因。(请参见5.5 当前实现的已知限制。)
对于 Fortran,如果由 __auto 或 default(__auto) 子句自动确定某个变量的作用域,并且该变量具有根据 OpenMP 规范预先确定的作用域,则编译器将根据该预先确定的作用域来确定变量的作用域。
对于 Fortran,下列变量具有预先确定的作用域:
threadprivate 指令中出现的变量和通用块为 threadprivate。
DO 或 PARALLEL DO 构造的 DO 循环中的循环迭代变量在该构造中为 private。
在并行构造的顺序循环中作为循环迭代变量使用的变量在该并行构造中为 private。
隐含的 DO 或 FORALL 索引为 private。
Cray pointee 会继承其 Cray Fortran 指针关联的存储的共享属性。
对于 C/C++,如果由 __auto 或 default(__auto) 子句自动确定某个变量的作用域,并且该变量具有根据 OpenMP 规范预先确定的作用域,则编译器将根据该预先确定的作用域来确定变量的作用域。
对于 C/C++,下列变量具有预先确定的作用域:
threadprivate 指令中出现的变量为 threadprivate。
在构造内部的作用域中声明的具有自动存储持续时间的变量为 private。
具有堆分配存储的变量为 shared。
静态数据成员为 shared。
for 或 parallel for 构造的 for 循环中的循环迭代变量在该构造中为 private。
具有 const 限定的类型,且不具有 mutable 成员的变量为 shared。
C 和 C++ 中的自动确定作用域仅适用于基本数据类型:整型、浮点型和指针。如果用户指定了要自动确定作用域的结构变量或类变量,则编译器会将该变量的作用域确定为 shared,并且封闭并行区域将由单个线程执行。
使用编译器注释可检查自动确定作用域的结果,并确定是否因自动确定作用域失败而导致将并行区域序列化。
使用 -g 调试选项编译时,编译器将生成行内注释。可以使用 er_src 命令查看这个生成的注释,如下所示。(er_src 命令作为 Sun Studio 软件的一部分提供;有关更多信息,请参见 er_src(1) 手册页或《Sun Studio 性能分析器》手册。)
使用 -xvpara 选项进行编译是一个良好的开端。如果自动确定作用域失败,将打印一条警告消息(如下所示)。
%cat t.f INTEGER X(100), Y(100), I, T C$OMP PARALLEL DO DEFAULT(__AUTO) DO I=1, 100 T = Y(I) CALL FOO(X) X(I) = T*T END DO C$OMP END PARALLEL DO END %f95 -xopenmp -xO3 -vpara -c t.f "t.f", line 2: Warning: parallel region will be executed by a single thread because the autoscoping of following variables failed - x |
使用 f95 的 -vpara 或 cc 的 -xvpara 进行编译。(CC 中尚未实现此选项。)
%cat t.f INTEGER X(100), Y(100), I, T C$OMP PARALLEL DO DEFAULT(__AUTO) DO I=1, 100 T = Y(I) X(I) = T*T END DO C$OMP END PARALLEL DO END %f95 -xopenmp -xO3 -g -c t.f %er_src t.o Source file: ./t.f Object file: ./ot.o Load Object: ./t.o 1. INTEGER X(100), Y(100), I, T Source OpenMP region below has tag R1 Variables autoscoped as SHARED in R1: x, y Variables autoscoped as PRIVATE in R1: t, i Private variables in R1: i, t Shared variables in R1: y, x 2. C$OMP PARALLEL DO DEFAULT(__AUTO) <Function: _$d1A2.MAIN_> Source loop below has tag L1 L1 parallelized by explicit user directive L1 parallel loop-body code placed in function _$d1A2.MAIN_ along with 0 inner loops Copy in M-function of loop below has tag L2 L2 scheduled with steady-state cycle count = 3 L2 unrolled 4 times L2 has 0 loads, 0 stores, 2 prefetches, 0 FPadds, 0 FPmuls, and 0 FPdivs per iteration L2 has 1 int-loads, 1 int-stores, 4 alu-ops, 1 muls, 0 int-divs and 1 shifts per iteration 3. DO I=1, 100 4. T = Y(I) 5. X(I) = T*T 6. END DO 7. C$OMP END PARALLEL DO 8. END |
接下来,使用一个更复杂的示例来解释自动作用域规则的应用方式。
1. REAL FUNCTION FOO (N, X, Y) 2. INTEGER N, I 3. REAL X(*), Y(*) 4. REAL W, MM, M 5. 6. W = 0.0 7. 8. C$OMP PARALLEL DEFAULT(__AUTO) 9. 10. C$OMP SINGLE 11. M = 0.0 12. C$OMP END SINGLE 13. 14. MM = 0.0 15. 16. C$OMP DO 17. DO I = 1, N 18. T = X(I) 19. Y(I) = T 20. IF (MM .GT. T) THEN 21. W = W + T 22. MM = T 23. END IF 24. END DO 25. C$OMP END DO 26. 27. C$OMP CRITICAL 28. IF ( MM .GT. M ) THEN 29. M = MM 30. END IF 31. C$OMP END CRITICAL 32. 33. C$OMP END PARALLEL 34. 35. FOO = W - M 36. 37. RETURN 38. END |
函数 FOO() 包含一个并行区域,该并行区域包含一个 SINGLE 构造、一个工作共享 DO 构造和一个 CRITICAL 构造。如果我们忽略所有 OpenMP 并行构造,则并行区域中的代码所执行的操作如下:
将数组 X 中的值复制到数组 Y
查找 X 中的最大正数值,并将其存储在 M 中
将 X 的一些元素的值累加到变量 W 中。
让我们看一下编译器如何使用上述规则来查找并行区域中变量的相应作用域。
在并行区域中使用下列变量:I、N、MM、T、W、M、X 和 Y。编译器将确定以下内容。
标量 I 是工作共享 DO 循环的循环索引。OpenMP 规范要求将 I 的作用域确定为 PRIVATE。
标量 N 在并行区域中只被进行读取,因此不会导致数据争用,这样,按照规则 S1,将其作用域确定为 SHARED。
任何执行并行区域的线程都会执行语句 14,以便将标量 MM 的值设置为 0.0。这一写入操作会导致数据争用,因此规则 S1 不适用。由于是在同一线程中读取 MM 之前出现该写入操作,因此,按照规则 S2,将 MM 的作用域确定为 PRIVATE。
同样,将标量 T 的作用域确定为 PRIVATE。
先读取标量 W,然后将其写入语句 21,因此,规则 S1 和 S2 不适用。加法运算同时符合结合律和交换律,因此,按照规则 S3,将 W 的作用域确定为 REDUCTION(+)。
标量 M 写入语句 11,该语句位于 SINGLE 构造内。SINGLE 构造末尾的隐式屏障可确保写入语句 11 不会与读取语句 28 或写入语句 29 同时发生,并且后两者不会同时发生,因为它们都位于同一 CRITICAL 构造内。没有任何两个线程可以同时访问 M。因此,在并行区域中写入和读取 M 都不会导致数据争用,按照规则 S1,将 M 的作用域确定为 SHARED。
数组 X 在区域中只被进行读取,不进行写入,因此,按照规则 A1,将其作用域确定为 SHARED。
写入数组 Y 的操作分布在各个线程中,没有任何两个线程会写入 Y 的相同元素。由于不存在数据争用,因此,按照规则 A1,将 Y 的作用域确定为 SHARED。
以下是当前 Sun Studio Fortran 95 编译器中已知的自动确定作用域限制。
只识别 OpenMP 指令,并且只能在分析中使用。无法识别对 OpenMP 运行时例程的调用。例如,如果程序使用 OMP_SET_LOCK() 和 OMP_UNSET_LOCK() 来实现临界段,则编译器无法检测出是否存在临界段。如果可能,请使用 CRITICAL 和 END CRITICAL 指令。
在分析过程中,只能识别和使用通过 OpenMP 同步指令(如 BARRIER 和 MASTER)指定的同步。不识别用户实现的同步,如忙等待。
使用 -xopenmp=noopt 编译时不支持自动确定作用域。