Oracle Solaris Studio 12.2: OpenMP API ユーザーガイド

第 6 章 変数の自動スコープ宣言

OpenMP 構文で参照される変数のデータ共有属性の宣言は、スコープ宣言と呼ばれます。各データ共有属性の説明は、OpenMP 3.0 仕様の 2.9.3 節にあります。

OpenMP プログラムでは、OpenMP 構文で参照されるすべての変数は、スコープ宣言されます。一般的に、構文で参照される変数は、2 つの方法のうちのどちらかでスコープ宣言されます。プログラマがデータ共有属性節で変数のスコープを明示的に宣言するか、あるいは OpenMP API がコンパイラに実装されていることで、暗黙に決まるか事前定義されたスコープに対して規則が自動的に適用されます。これは OpenMP 3.0 仕様の 2.9.1 節に基づいています。

ほとんどのユーザーにとって、OpenMP パラダイムの中でもっとも困難なのは、スコープ宣言です。変数を明示的にスコープ宣言することは、特に大規模で複雑なプログラムの場合、手間がかかりミスもしやすくなります。さらに、OpenMP 3.0 の仕様では、暗黙に決まるか事前定義された変数スコープに対する規則が指定されているため、これにより予期せぬ結果を招くことがあります。OpenMP 仕様 3.0 で task 指令が追加されたことにより、スコープ宣言がさらに複雑で難しくなりました。

自動的にスコープ宣言を行う、自動スコープ宣言と呼ばれる機能が Solaris Studio コンパイラでサポートされています。これは非常に便利なツールで、プログラマは変数のスコープを明示的に定義しなくても済みます。自動スコープ宣言では、コンパイラは非常にシンプルなユーザーモデルで、スマートな規則に基づいて変数のスコープを決定します。

コンパイラの過去のリリースでは、変数の自動スコープ宣言は parallel 構文でしか行えませんでした。最新の Solaris Studio コンパイラでは、自動スコープ宣言機能が task 構文で参照される変数にも拡張されました。

6.1 自動スコープ宣言用データスコープ節

自動スコープ宣言は、自動的にスコープ宣言される変数を __auto データスコープで指定するか、default(__auto) 節を使用することで呼び出されます。どちらも、Solaris Studio コンパイラで提供される OpenMP で拡張された仕様です。

6.1.1 __auto 節

構文: __auto(list-of-variables)

Fortran の場合、__AUTO(list-of-variables) も使用できます。

並列構文またはタスク構文上の __auto 節は、コンパイラが構文中で指定された変数のスコープを自動的に特定するように指示します。auto の前の下線は 2 つであることに注意してください。

__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 task 構文のスコープ宣言規則

自動スコープ宣言では、コンパイラは、task 構文内の変数のスコープを決定する際に次の規則を適用します。

これらの規則は、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) 節を parallel 構文に対して指定しても、parallel 構文に字句的または動的に包含される task 構文にも同じ節が適用されることを意味するわけではありません。

事前定義された暗黙的スコープを持たない変数を自動スコープ宣言する場合、コンパイラは、変数の使用について、前述の規則に対して順番に確認します。規則に一致する場合、コンパイラはその規則に従って変数のスコープを決定します。一致する規則がない場合、または自動スコープ宣言が変数を処理できない場合 (ただし、後述のように、いくつかの制限事項はあります)、コンパイラは変数を SHARED とスコープ宣言し、parallel または task 構文を IF (.FALSE.) または if(0) 節が指定されているかのように結合並列領域が直列化されます。

自動スコープ宣言が失敗する理由は、一般的に 2 つあります。1 つは、変数の使われ方が前述のどれにも一致しないため、もう 1 つは、ソースコードが複雑すぎて、コンパイラが十分な解析を行えないためです。こうした原因としてよくあるのは、たとえば、関数呼び出しや複雑な配列添え字、メモリー別名、ユーザー実装の同期などです。

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() には並列領域が 1 つあり、この並列領域には、SINGLE 構文とワークシェアリングの DO 構文、CRITICAL 構文がそれぞれ 1 つあります。こうした 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 もう 1 つの例


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 はタスク構文でも並列構文でも読み取り専用です。そのため、n は TS1 に従って FIRSTPRIVATE と自動スコープ宣言されます。

スカラー変数 x および y は、ローカル関数 fib() のローカル変数です。両方のタスクが x と y にアクセスしても、データ競合は起こりません。taskwait があるため、2 つのタスクがまず実行を完了してから、タスクを検出後 fib() を実行していたスレッドが fib() を終了します。つまり、x と y は、2 つのタスクが実行している間でも利用可能なのです。そのため、x と y は、TS2 に従い、SHARED と自動スコープ宣言されます。



例 6–6 もう 1 つの例


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 は並列領域の private 変数です。チームのスレッドの 1 つが xx の初期値を変更します (task1 を実行します)。その後、すべてのスレッドが task2 を検出し、xx が同じ計算を行います。

task1 では、xx を使用しても、データ競合は発生しません。1 つの構文の終わりに暗黙的なバリアーがあり、このバリアーを出る前に task1 を完了する必要があるので、xx は task1 が実行している間でも利用可能なのです。したがって、TS2 により、xx は task1 で SHARED と自動スコープ宣言されます。

task2 では、xx は読み取り専用として使用されます。ただし、xx の使用は、包含する並列構文では読み取り専用ではありません。xx は、並列構文に対しては PRIVATE と事前定義されているので、task2 が実行している間でも xx が利用可能かどうかはわかりません。そのため、TS3 に従い、task2 では xx は FIRSTPRIVATE と自動スコープ宣言されます。

task2 では、yy を使用することでデータ競合が発生し、task2 を実行する各スレッドでは、変数 yy は同じスレッドによる読み取りの前に常に書き込まれます。そのため、TS4 に従い、yy は task2 では PRIVATE と自動スコープ宣言されます。



例 6–7 もう 1 つの例


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

task 構文では xx は読み取り専用として使用されないので、データ競合が発生します。しかし、タスク領域で x を読み取ると、タスク外で x の値が定義されます(この例では、xx は並列領域に対して SHARED なので、x の定義は並列領域外で行われます)。そのため、TS5 に従い、xx は FIRSTPRIVATE と自動スコープ宣言されます。

task 構文での yy の使用は読み取り専用ではありませんが、データ競合は発生しません。taskwait が発生しているので、yy はタスクの実行中でもアクセスできます。そのため、TS2 に従い、yy は SHARED と自動スコープ宣言されます。