声明 OpenMP 构造中所引用变量的数据共享属性的过程称为确定作用域。有关每个数据共享属性的说明,请参见 OpenMP 3.0 规范的第 2.9.3 节。
在 OpenMP 程序中,会为 OpenMP 构造中引用的每个变量确定作用域。通常,可通过两种方法之一来确定构造中所引用变量的作用域。程序员可使用数据共享属性子句来显式声明变量的作用域,或者,编译器中的 OpenMP API 实现可根据 OpenMP 3.0 规范的第 2.9.1 节自动对预先确定或隐式确定的作用域应用规则。
大多数用户会发现,确定作用域是使用 OpenMP 范例中最难的部分。显式确定变量作用域的过程非常乏味,而且容易出错,尤其是对于大型和复杂的程序而言。而且,在 OpenMP 3.0 规范中针对隐式确定和预先确定变量作用域所指定的规则可能会产生某些意外结果。OpenMP 3.0 规范中引入的 task 指令增加了确定作用域的复杂性和难度。
自动确定作用域功能称为自动作用域,受 Solaris Studio 编译器支持,使得程序员无需显式确定变量的作用域,因此是一个很有用的工具。借助自动作用域功能,编译器可在非常简单的用户模型中使用一些智能规则来确定变量作用域。
早期编译器版本将自动作用域仅限于并行构造中的变量。当前的 Solaris Studio 编译器对自动作用域功能的范围进行了扩展,包含了任务构造中引用的变量。
可通过在 __auto 数据作用域子句中指定要自动确定作用域的变量或使用 default(__auto) 子句,来调用自动作用域功能。这两种方法都是 Solaris Studio 编译器提供的 OpenMP 规范的扩展。
语法: __auto(list-of-variables)
对于 Fortran,也接受 __AUTO(list-of-variables)。
并行或任务构造中的 __auto 子句可指示编译器自动确定构造中已命名变量的作用域。(请注意 auto 前面的两个下划线)。
__auto 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS、Fortran 95 PARALLEL WORKSHARE 或 TASK 指令中。
如果在 __auto 子句中指定了变量,将不能在任何其他数据共享属性子句中指定该变量。
语法:default(__auto)
对于 Fortran,也接受 DEFAULT(__AUTO)。
并行或任务构造中的 default(__auto) 子句可指示编译器自动确定构造中引用的所有未在任何数据作用域子句中显式确定作用域的变量的作用域。
default(__auto) 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS、Fortran 95 PARALLEL WORKSHARE 或 TASK 指令中。
在自动确定作用域的情况下,编译器应用以下规则来确定并行构造中变量的作用域。
这些规则并不适用于由 OpenMP 规范隐式确定作用域的变量,如工作共享 DO 循环或 FOR 循环的循环索引变量。
在自动确定并行构造中引用的且没有预先确定或隐式确定作用域的标量变量的作用域时,编译器会按给定顺序根据以下规则 PS1-PS3 来检查变量的使用。
PS1:对于组中执行并行区域的线程而言,如果在该区域中使用变量不会导致数据争用情形,则将变量的作用域确定为 SHARED。
PS2:如果在每个执行并行区域的线程中,在读取变量之前始终先由同一线程写入,则将变量的作用域确定为 PRIVATE。如果可以将变量的作用域确定为 PRIVATE,并且该变量在写入(在并行区域之后写入)之前被读取,而构造为 PARALLEL DO 或 PARALLEL SECTIONS,则将其作用域确定为 LASTPRIVATE。
PS3:如果在编译器可以识别的归约操作中使用变量,则将该变量的作用域确定为具有该特定操作类型的 REDUCTION。
PA1:对于组中执行并行区域的线程而言,如果在该区域中使用数组不会导致数据争用情形,则将数组的作用域确定为 SHARED。
在自动确定作用域的情况下,编译器应用以下规则来确定任务构造中变量的作用域。
这些规则不适用于由 OpenMP 规范隐式确定作用域的变量,如 PARALLEL DO/for 循环的循环索引变量。
在自动确定任务构造中引用的且没有预先确定或隐式确定作用域的标量变量的作用域时,编译器会按给定顺序根据以下规则 TS1-TS5 来检查变量的使用。
TS1:如果变量的使用在任务构造中是只读的,并且在包括该任务构造的并行构造中也是只读的,则自动将变量的作用域确定为 FIRSTPRIVATE。
TS2:如果变量的使用不会导致数据争用,并且可在执行任务时访问该变量,则自动将变量的作用域确定为 SHARED。
TS3:如果变量的使用不会导致数据争用,并且在任务构造中是只读的,但在执行任务时不可访问该变量,则自动将变量的作用域确定为 FIRSTPRIVATE。
TS4:如果变量的使用会导致数据争用,并且在每个执行任务区域的线程中,在读取变量之前始终先由同一线程写入,则自动将变量的作用域确定为 PRIVATE。
TS5:如果变量的使用会导致数据争用,在任务区域中不是只读的,并且在任务区域中执行某些读取操作可能会获取在任务之外定义的值,则自动将变量的作用域确定为 FIRSTPRIVATE。
自动确定任务的作用域时不会处理数组。
注意,在将来的版本中,任务自动作用域规则和自动作用域结果可能会有所更改。而且,隐式确定的作用域规则和自动作用域规则的应用顺序在将来的版本中也会发生更改。
程序员使用 _auto(list-of-variables) 子句或 default(_auto) 子句显式请求自动作用域。为并行构造指定 default(_auto) 或 _auto(list-of-variables) 子句,并不意味着将同一子句应用于在语法上或动态包含在并行构造中的任务构造。
在对没有预先确定隐式作用域的变量自动确定作用域时,编译器会按给定顺序根据上述规则来检查变量的使用。如果符合某个规则,编译器将按照匹配的规则确定变量的作用域。如果没有匹配的规则或自动作用域无法处理变量(如下所述,存在某些限制),编译器会将变量的作用域确定为 SHARED,并将并行或任务构造视为如同指定了 IF (.FALSE.) 或 if(0) 子句一样。
通常,自动确定作用域失败的原因有两个。一个原因是使用的变量不匹配任何规则。第二个原因是源代码对于编译器来说过于复杂,因而无法执行全面的分析。函数调用、复杂的数组下标、内存别名和用户实现的同步都是常见原因。
要启用自动作用域,必须使用 -xopenmp 在优化级别 -xO3 或更高级别上编译程序。如果仅使用 -xopenmp=noopt 编译程序,将不会启用自动作用域。
C 和 C++ 中的并行和任务自动作用域只能处理基本数据类型:整型、浮点和指针。
任务自动作用域不能处理数组。
C 和 C++ 中的任务自动作用域不能处理全局变量。
任务自动作用域不能处理非绑定任务。
任务自动作用域不能处理在语法上包含在其他任务中的任务。例如:
#pragma omp task /* task1 */ { ... #pragma omp task /* task 2 */ { ... } ... } |
在以上示例中,由于 task2 在语法上嵌套在 task1 中,因此编译器不会尝试对其启用自动作用域。编译器会将 task2 中引用的所有变量的作用域确定为 SHARED,并将 task2 视为如同指定了 IF(.FALSE.) 或 if(0) 子句一样。
只识别 OpenMP 指令,并且只能在分析中使用。无法识别对 OpenMP 运行时例程的调用。例如,如果程序使用 omp_set_lock() 和 omp_unset_lock() 来实现临界段,编译器将无法检测是否存在临界段。如果可能,请使用 CRITICAL 和 END CRITICAL 指令。
在数据争用分析中,只能识别和使用通过 OpenMP 同步指令(如 BARRIER 和 MASTER)指定的同步。不识别用户实现的同步,如忙等待。
使用编译器注释可检查自动作用域结果,并确定是否因自动作用域失败而对并行区域进行了序列化。
使用 -g 调试选项编译时,编译器将生成行内注释。可以使用 er_src 命令查看这个生成的注释,如下所示。(er_src 命令作为 Solaris Studio 软件的一部分提供;有关更多信息,请参见 er_src(1) 手册页或《Solaris Studio 性能分析器》手册)。
使用 -xvpara 选项进行编译是一个良好的开端。使用 -xvpara 进行编译可大体确定针对特定构造的自动作用域是否成功。以下是一个示例:
%cat source1.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 -vpara -c -g source1.f "source1.f", line 2: Autoscoping for OpenMP construct succeeded. Check er_src for details |
如果针对特定构造的自动作用域失败,将发出本示例中所示的警告消息(使用 -xvpara):
%cat source2.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 -g source2.f "source2.f", line 2: Warning: Autoscoping for OpenMP construct failed. Check er-src for details. Parallel region will be executed by a single thread. |
er_src 所显示的编译器注释中显示了更详细的信息:
% er_src source2.o Source file: source2.f Object file: source2.o Load Object: source2.o 1. INTEGER X(100), Y(100), I, T Source OpenMP region below has tag R1 Variables autoscoped as SHARED in R1: y Variables autoscoped as PRIVATE in R1: t, i Variables treated as shared because they cannot be autoscoped in R1: x R1 will be executed by a single thread because autoscoping for some variable s was not successful Private variables in R1: i, t Shared variables in R1: y, x 2. C$OMP PARALLEL DO DEFAULT(__AUTO) Source loop below has tag L1 L1 parallelized by explicit user directive L1 autoparallelized L1 parallel loop-body code placed in function _$d1A2.MAIN_ along with 0 inne r loops L1 could not be pipelined because it contains calls 3. DO I=1, 100 4. T = Y(I) 5. CALL FOO(X) 6. X(I) = T*T 7. END DO 8. C$OMP END PARALLEL DO 9. END 10. |
下面的示例说明了自动作用域规则的工作原理。
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。
static void par_quick_sort (int p, int r, float *data) { if (p < r) { int q = partition (p, r, data); #pragma omp task default(__auto) if ((r-p)>=low_limit) par_quick_sort (p, q-1, data); #pragma omp task default(__auto) if ((r-p)>=low_limit) par_quick_sort (q+1, r, data); } } int main () { ... #pragma omp parallel { #pragma omp single nowait par_quick_sort (0, N-1, &Data[0]); } ... } er_src result: Source OpenMP region below has tag R1 Variables autoscoped as FIRSTPRIVATE in R1: p, q, data Firstprivate variables in R1: data, p, q 47. #pragma omp task default(__auto) if ((r-p)>=low_limit) 48. par_quick_sort (p, q-1, data); Source OpenMP region below has tag R2 Variables autoscoped as FIRSTPRIVATE in R2: q, r, data Firstprivate variables in R2: data, q, r 49. #pragma omp task default(__auto) if ((r-p)>=low_limit) 50. par_quick_sort (q+1, r, data); |
标量变量 p 和 q 以及指针变量数据在任务构造和并行区域中都是只读的。因此,根据 TS1 自动将其作用域确定为 FIRSTPRIVATE。
int fib (int n) { int x, y; if (n < 2) return n; #pragma omp task default(__auto) x = fib(n - 1); #pragma omp task default(__auto) y = fib(n - 2); #pragma omp taskwait return x + y; } er_src result: Source OpenMP region below has tag R1 Variables autoscoped as SHARED in R1: x Variables autoscoped as FIRSTPRIVATE in R1: n Shared variables in R1: x Firstprivate variables in R1: n 24. #pragma omp task default(__auto) /* shared(x) firstprivate(n) */ 25. x = fib(n - 1); Source OpenMP region below has tag R2 Variables autoscoped as SHARED in R2: y Variables autoscoped as FIRSTPRIVATE in R2: n Shared variables in R2: y Firstprivate variables in R2: n 26. #pragma omp task default(__auto) /* shared(y) firstprivate(n) */ 27. y = fib(n - 2); 28. 29. #pragma omp taskwait 30. return x + y; 31. } |
标量 n 在任务构造和并行构造中都是只读的。因此,根据 TS1 自动将 n 的作用域确定为 FIRSTPRIVATE。
标量变量 x 和 y 是函数 fib() 的局部变量。在两个任务中访问 x 和 y 不会导致数据争用。由于存在一个 taskwait,因此这两个任务将在执行 fib() 的线程(遇到了这两个任务)退出 fib() 之前完成执行;这意味着 x 和 y 将在这两个任务执行时出现。因此,根据 TS2 自动将 x 和 y 的作用域确定为 SHARED。
int main(void) { int yy = 0; #pragma omp parallel default(__auto) shared(yy) { int xx = 0; #pragma omp single { #pragma omp task default(__auto) // task1 { xx = 20; } } #pragma omp task default(__auto) // task2 { yy = xx; } } return 0; } er_src result: Source OpenMP region below has tag R1 Variables autoscoped as PRIVATE in R1: xx Private variables in R1: xx Shared variables in R1: yy 7. #pragma omp parallel default(__auto) shared(yy) 8. { 9. int xx = 0; 10. Source OpenMP region below has tag R2 11. #pragma omp single 12. { Source OpenMP region below has tag R3 Variables autoscoped as SHARED in R3: xx Shared variables in R3: xx 13. #pragma omp task default(__auto) // task1 14. { 15. xx = 20; 16. } 17. } 18. Source OpenMP region below has tag R4 Variables autoscoped as PRIVATE in R4: yy Variables autoscoped as FIRSTPRIVATE in R4: xx Private variables in R4: yy Firstprivate variables in R4: xx 19. #pragma omp task default(__auto) // task2 20. { 21. yy = xx; 22. } 23. } |
在此示例中,xx 是并行区域中的专用变量。组中的其中一个线程会修改 xx 的初始值(通过执行 task1)。然后,所有线程都会遇到使用 xx 执行某些计算的 task2。
在 task1 中,使用 xx 不会导致数据争用。由于单个构造结尾有一个隐式屏障,而且 task1 应在退出此屏障之前完成,因此,xx 将在 task1 执行时出现。所以,根据 TS2,在 task1 中自动将 xx 的作用域确定为 SHARED。
在 task2 中,xx 的使用是只读的。但是,xx 的使用在包含它的并行构造中不是只读的。由于 xx 在并行构造中预先确定为 PRIVATE,因此无法确定 xx 将在执行 task2 时出现。所以,根据 TS3,在 task2 中自动将 xx 的作用域确定为 FIRSTPRIVATE。
在 task2 中,使用 yy 会导致数据争用,在每个执行 task2 的线程中,在读取变量 yy 之前始终先由同一线程写入该变量。所以,根据 TS4,在 task2 中自动将 yy 的作用域确定为 PRIVATE。
int foo(void) { int xx = 1, yy = 0; #pragma omp parallel shared(xx,yy) { #pragma omp task default(__auto) { xx += 1; #pragma omp atomic yy += xx; } #pragma omp taskwait } return 0; } er_src result: Source OpenMP region below has tag R1 Shared variables in R1: yy, xx 5. #pragma omp parallel shared(xx,yy) 6. { Source OpenMP region below has tag R2 Variables autoscoped as SHARED in R2: yy Variables autoscoped as FIRSTPRIVATE in R2: xx Shared variables in R2: yy Firstprivate variables in R2: xx 7. #pragma omp task default(__auto) 8. { 9. xx += 1; 10. 11. #pragma omp atomic 12. yy += xx; 13. } 14. 15. #pragma omp taskwait 16. } |
在任务构造中,xx 的使用不是只读的,且会导致数据争用。但在任务区域中读取 x 会获取在任务之外定义的 x 值。(在此示例中,由于 xx 对于并行区域确定为 SHARED,因此 x 的定义实际上在并行区域之外。)所以,根据 TS5,自动将 xx 的作用域确定为 FIRSTPRIVATE。
在任务构造中,yy 的使用不是只读的,但不会导致数据争用。由于存在 taskwait,因此可在执行任务时访问 yy。所以,根据 TS2,自动将 yy 的作用域确定为 SHARED。