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 構文で参照される変数にも拡張されました。
自動スコープ宣言は、自動的にスコープ宣言される変数を __auto データスコープで指定するか、default(__auto) 節を使用することで呼び出されます。どちらも、Solaris Studio コンパイラで提供される OpenMP で拡張された仕様です。
構文: __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 節で変数を指定した場合、ほかのデータ共有属性節でその変数を指定できません。
構文: 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 と宣言されます。
自動スコープ宣言では、コンパイラは、task 構文内の変数のスコープを決定する際に次の規則を適用します。
これらの規則は、OpenMP 仕様で暗黙にスコープ宣言される、PARALLEL DO や FOR ループのループインデックス変数などの変数には適用されません。
タスク構文で参照され、暗黙に決まるか事前定義されたスコープを持たないスカラー変数を自動スコープ宣言する場合、コンパイラは、変数の使用を次の規則 TS1 - TS5 に対して順番に確認します。
TS1: task 構文内で変数が読み取り専用として使用され、その task 構文を包含する並列構文内でも読み取り専用である場合、その変数は FIRSTPRIVATE として自動スコープ宣言されます。
TS2: 変数を使用してもデータ競合が発生せず、タスクの実行中にその変数にアクセスできる場合、その変数は SHARED と自動スコープ宣言されます。
TS3: 変数を使用してもデータ競合が発生せず、タスク構文で読み取り専用であり、かつ、タスクの実行中には変数にアクセスできない場合は、その変数は FIRSTPRIVATE と自動スコープ宣言されます。
TS4: 変数を使用するとデータ競合が発生し、タスク領域を実行する各スレッドで、その変数が同じスレッドによる読み取りの前に常に書き込まれる場合、その変数は PRIVATE と自動スコープ宣言されます。
TS5: 変数を使用するとデータ競合が発生し、タスク領域内で読み取り専用ではなく、かつ、タスク領域で行われる読み取りの中で、タスク外で定義された値が取得されることがある場合は、その変数は FIRSTPRIVATE と自動スコープ宣言されます。
タスクの自動スコープ宣言では、配列は処理しません。
タスクの自動スコープ宣言規則と、自動スコープ宣言の結果は、今後のリリースで変更される可能性があります。また、暗黙的に決定されたスコープ宣言規則と自動スコープ宣言規則が適用される順序も、今後のリリースで変更される可能性があります。
プログラマは _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 つは、ソースコードが複雑すぎて、コンパイラが十分な解析を行えないためです。こうした原因としてよくあるのは、たとえば、関数呼び出しや複雑な配列添え字、メモリー別名、ユーザー実装の同期などです。
自動スコープ宣言を有効にするには、最適化レベルを -xO3 かそれ以上に設定してから -xopenmp でプログラムをコンパイルする必要があります。自動スコープ宣言は、プログラムが -xopenmp=noopt だけでコンパイルされている場合は有効になりません。
C および C++ の並列およびタスク構文の自動スコープ宣言では、基本的なデータ型、つまり整数型、浮動小数点型、およびポインタ型しか処理できません。
タスクの自動スコープ宣言では、配列は処理できません。
C および C++ でのタスクの自動スコープ宣言では、グローバル変数は処理できません。
タスクの自動スコープ宣言では、結合解除されたタスクは処理できません。
タスクの自動スコープ宣言では、ほかのタスクに字句的に包含されているタスクは処理できません。次に例を示します。
#pragma omp task /* task1 */ { ... #pragma omp task /* task 2 */ { ... } ... } |
前述の例では、コンパイラは、task1 に字句的に入れ子になった task2 の自動スコープは試行しません。コンパイラは task2 で参照されているすべての変数を SHARED とスコープ宣言し、task2 を IF(.FALSE.) または if(0) 節がタスクで指定されているかのように処理します。
解析では、OpenMP 指令のみ認識、使用されます。OpenMP 実行時ルーチンの呼び出しは認識されません。たとえばプログラムが omp_set_lock() および omp_unset_lock() を使用してクリティカル領域を実装している場合、コンパイラはそのクリティカル領域の存在を検出できません。可能な場合は、CRITICAL および END CRITICAL 指令を使用してください。
データ競合解析では、BARRIER や MASTER などの OpenMP 同期指令を使用して指定された同期のみ認識、使用されます。ビジー待ちなどのユーザー実装の同期は認識されません。
自動スコープ宣言の結果を確認したり、自動スコープ宣言が失敗したために直列化した並列領域の有無を確認したりするには、コンパイラのコメントを使用します。
コンパイルで -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() には並列領域が 1 つあり、この並列領域には、SINGLE 構文とワークシェアリングの DO 構文、CRITICAL 構文がそれぞれ 1 つあります。こうした OpenMP 並列構文をすべて無視した場合、並列領域内のコードが行うのは、次のことです。
配列 X 内の値を配列 Y にコピーします。
X 内の正の最大値を検出し、その値を M に格納します。
X の一部要素の値を変数 W に蓄積します。
コンパイラが前述の規則に従って、この並列領域内の変数に適切なスコープを発見する仕組みをみてみましょう。
前述の並列領域では、I、N、MM、T、W、M、X、および Y という変数が使用されています。コンパイラは次のことを決定します。
スカラー I は、ワークシェアリング DO ループのループインデックスです。OpenMP 仕様では、I のスコープは PRIVATE 宣言することが必須です。
スカラー N は並列領域内で読み取られるだけで、データ競合を起こしません。このため、規則 S1 に従って、この変数のスコープは SHARED と宣言されます。
並列領域を実行するスレッドはすべて、スカラー MM の値を 0.0 に設定する 14 行目を実行します。この書き込みはデータ競合の原因になるため、規則 S1 は適用されません。この書き込みは、同じスレッド内のあらゆる MM の読み取りの前に起きるため、規則 S2 に従って、MM のスコープは PRIVATE と宣言されます。
同様に、T も PRIVATE とスコープ宣言されます。
スカラー W は 21 行目でいったん読み取られたあとに書き込まれます。このため、S1 および S2 は適用されません。加算は連想および伝達の両方の要素が含まれるため、規則 S3 に従って W のスコープは REDUCTION(+) と宣言されます。
スカラー M は、SINGLE 構文にある文 11 で書き込まれます。この SINGLE 構文の末尾のバリアは、文 11 の書き込みが文 28 の読み取りや文 29 の書き込みと同時に発生しないようにするためのものです。また、文 28 と 29 はどちらも CRITICAL 構文内にあるため、同時に発生しないようになっています。2 つのスレッドが同時に M にアクセスすることはできません。このため、並列領域内での M 読み取りと書き込みがデータ競合を起こすことはなく、規則 S1 に従って、M のスコープは SHARED と宣言されます。
配列 X は領域内では読み取りだけで、書き込みは行われません。このため、この配列のスコープは、規則 A1 に従って SHARED と宣言されます。
配列 Y への書き込みはスレッド間で分散され、2 つのスレッドが Y の同じ要素に書き込むことはありません。データの競合がないため、Y のスコープは、規則 A1 に従って 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 はタスク構文でも並列構文でも読み取り専用です。そのため、n は TS1 に従って FIRSTPRIVATE と自動スコープ宣言されます。
スカラー変数 x および y は、ローカル関数 fib() のローカル変数です。両方のタスクが x と y にアクセスしても、データ競合は起こりません。taskwait があるため、2 つのタスクがまず実行を完了してから、タスクを検出後 fib() を実行していたスレッドが fib() を終了します。つまり、x と y は、2 つのタスクが実行している間でも利用可能なのです。そのため、x と y は、TS2 に従い、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 は並列領域の 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 と自動スコープ宣言されます。
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 と自動スコープ宣言されます。