Oracle Solaris Studio 12.2:OpenMP API 用户指南

第 6 章 自动确定变量的作用域

声明 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 编译器对自动作用域功能的范围进行了扩展,包含了任务构造中引用的变量。

6.1 自动作用域数据范围子句

可通过在 __auto 数据作用域子句中指定要自动确定作用域的变量或使用 default(__auto) 子句,来调用自动作用域功能。这两种方法都是 Solaris Studio 编译器提供的 OpenMP 规范的扩展。

6.1.1 __auto 子句

语法: __auto(list-of-variables)

对于 Fortran,也接受 __AUTO(list-of-variables)。

并行或任务构造中的 __auto 子句可指示编译器自动确定构造中已命名变量的作用域。(请注意 auto 前面的两个下划线)。

__auto 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS、Fortran 95 PARALLEL WORKSHARE 或 TASK 指令中。

如果在 __auto 子句中指定了变量,将不能在任何其他数据共享属性子句中指定该变量。

6.1.2 default(__auto) 子句

语法:default(__auto)

对于 Fortran,也接受 DEFAULT(__AUTO)。

并行或任务构造中的 default(__auto) 子句可指示编译器自动确定构造中引用的所有未在任何数据作用域子句中显式确定作用域的变量的作用域。

default(__auto) 子句可以出现在 PARALLEL、PARALLEL DO/for、PARALLEL SECTIONS、Fortran 95 PARALLEL WORKSHARE 或 TASK 指令中。

6.2 并行构造的作用域规则

在自动确定作用域的情况下,编译器应用以下规则来确定并行构造中变量的作用域。

这些规则并不适用于由 OpenMP 规范隐式确定作用域的变量,如工作共享 DO 循环或 FOR 循环的循环索引变量。

6.2.1 标量变量的作用域规则

在自动确定并行构造中引用的且没有预先确定或隐式确定作用域的标量变量的作用域时,编译器会按给定顺序根据以下规则 PS1-PS3 来检查变量的使用。

6.2.2 数组的作用域规则

6.3 任务构造的作用域规则

在自动确定作用域的情况下,编译器应用以下规则来确定任务构造中变量的作用域。

这些规则不适用于由 OpenMP 规范隐式确定作用域的变量,如 PARALLEL DO/for 循环的循环索引变量。

6.3.1 标量变量的作用域规则

在自动确定任务构造中引用的且没有预先确定或隐式确定作用域的标量变量的作用域时,编译器会按给定顺序根据以下规则 TS1-TS5 来检查变量的使用。

6.3.2 数组的作用域规则

自动确定任务的作用域时不会处理数组。

6.4 关于自动作用域的通用注释

注意,在将来的版本中,任务自动作用域规则和自动作用域结果可能会有所更改。而且,隐式确定的作用域规则和自动作用域规则的应用顺序在将来的版本中也会发生更改。

程序员使用 _auto(list-of-variables) 子句或 default(_auto) 子句显式请求自动作用域。为并行构造指定 default(_auto) 或 _auto(list-of-variables) 子句,并不意味着将同一子句应用于在语法上或动态包含在并行构造中的任务构造。

在对没有预先确定隐式作用域的变量自动确定作用域时,编译器会按给定顺序根据上述规则来检查变量的使用。如果符合某个规则,编译器将按照匹配的规则确定变量的作用域。如果没有匹配的规则或自动作用域无法处理变量(如下所述,存在某些限制),编译器会将变量的作用域确定为 SHARED,并将并行或任务构造视为如同指定了 IF (.FALSE.) 或 if(0) 子句一样。

通常,自动确定作用域失败的原因有两个。一个原因是使用的变量不匹配任何规则。第二个原因是源代码对于编译器来说过于复杂,因而无法执行全面的分析。函数调用、复杂的数组下标、内存别名和用户实现的同步都是常见原因。

6.5 限制

6.6 检查自动作用域的结果

使用编译器注释可检查自动作用域结果,并确定是否因自动作用域失败而对并行区域进行了序列化。

使用 -g 调试选项编译时,编译器将生成行内注释。可以使用 er_src 命令查看这个生成的注释,如下所示。(er_src 命令作为 Solaris Studio 软件的一部分提供;有关更多信息,请参见 er_src(1) 手册页或《Solaris Studio 性能分析器》手册)。

使用 -xvpara 选项进行编译是一个良好的开端。使用 -xvpara 进行编译可大体确定针对特定构造的自动作用域是否成功。以下是一个示例:


示例 6–1 使用 -vpara 自动确定作用域


%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):


示例 6–2 使用 -vpara 自动确定作用域失败


%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.

6.7 自动作用域示例

下面的示例说明了自动作用域规则的工作原理。


示例 6–3 更复杂的示例


 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 并行构造,则并行区域中的代码所执行的操作如下:

  1. 将数组 X 中的值复制到数组 Y

  2. 查找 X 中的最大正数值,并将其存储在 M 中

  3. 将 X 的一些元素的值累加到变量 W 中。

让我们看一下编译器如何使用上述规则来查找并行区域中变量的相应作用域。

在并行区域中使用下列变量:I、N、MM、T、W、M、X 和 Y。编译器将确定以下内容。


示例 6–4 使用 QuickSort 的示例


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。



示例 6–5 另一个示例


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。



示例 6–6 另一个示例


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。



示例 6–7 另一个示例


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。