Sun MP C は、拡張 ANSI C コンパイラです。SPARC 共有メモリー付きマルチプロセッサマシン上で実行するコードをコンパイルすることができます。このプロセスは並列化と呼ばれています。コンパイル済みコードは、システム上の複数のプロセッサを用いて並列で実行することができます。
Sun WorkShop には、MP C の機能を使用するために必要なライセンスが含まれています。
この項では、MP C の概要と使用例を示すとともに、MP C で使用する環境変数、キーワード、プラグマ、オプションについて説明します。
MP C の使用例およびさらに詳しい参照情報については、/opt/SUNWspro/READMEs/ja/MPC.ps にある『MP C 技術白書』を参照してください。
MP C コンパイラによって、並列化しても安全であると判断されたループに対する並列コードが生成されます。通常、これらのループは、独立して実行可能な繰り返しを持っています。このようなループに対しては、繰り返しの実行される順番や、並列に実行するかどうかといったことなどは、実行結果に影響しません。すべてではありませんが、ほとんどのベクトル処理用ループはこのような種類のループです。
C では別名が存在する (複数の変数が同一の実体である、または実体を指す) 可能性があるため、並列化の安全性を判断することは困難です。コンパイラの作業を容易にするために、MP C には別名の情報をコンパイラに渡すためのプラグマおよびポインタ修飾子が用意されています。
以下の例では、MP C コンパイラを使用して、並列化を制御する様子を示しています。ターゲットのプログラムを並列化するためには、「-xautopar 」の -xautopar オプションを以下のように使用します。
% cc -fast -xO4 -xautopar example.c -o example
これを実行すると、example という実行可能ファイルが生成されます。ユーザーは通常の方法でこのファイルを実行することができます。
マルチプロセッサ上で実行する場合には、以下のように環境変数を設定することが必要になります。
% setenv PARALLEL 2
この設定によって、プログラムが 2 個のスレッド上で実行されるようになります。ターゲットのマシンに複数の CPU が装備されていれば、この設定によって、それぞれのスレッドがそれぞれの CPU にマップされます。
% example
プログラムを実行すると、2 個のスレッドが生成され、各スレッド上でプログラムの並列化された部分が実行されるようになります。
MP C ではキーワード _Restrict を使用することができます。詳細については、「_Restrict」の項を参照してください。
すでに述べたように、並列化の効果がどれだけあるかをコンパイラだけで決めるには、情報が不十分なことがあります。MP C では、プラグマをサポートしており、コンパイラだけでは不可能なループの並列化を効率よく実行することができます。
直列プラグマには 2 通りあり、どちらも for ループに適用されます。
#pragma MP serial_loop
#pragma MP serial_loop_nested
#pragma MP serial_loop
serial_loop プラグマによって、次に存在する for ループを暗黙的または自動的に並列化しないことが指示されます。
#pragma MP serial_loop_nested
serial_loop_nested プラグマによって、次にある for ループ、およびその for ループの中で入れ子になっている for ループを暗黙的または自動的に並列化しないことが指示されます。なお、serial_loop_nested のスコープは、このプラグマが適用されるループの範囲を越えることはありません。
並列プラグマは 1 つだけあります。
#pragma MP taskloop (オプション)
MP taskloop プラグマは、オプションとして、以下の引数を取ることができます。
maxcpus (<CPU 数>)
private (<スレッド固有変数リスト>)
shared (<共有変数リスト>)
readonly (<読み取り専用変数リスト>)
storeback (<ストアバック変数リスト>)
savelast
reduction (<縮約変数リスト>)
schedtype (<スケジューリング型>)
MP taskloop プラグマ 1 つに対して指定できるオプションは 1 つだけです。ただし、複数のプラグマの効果を重ねて、ソースコード内の現在のブロックにある次の for ループに適用することができます。
#pragma MP taskloop maxcpus(4) #pragma MP taskloop shared(a,b) #pragma MP taskloop storeback(x)
これらのオプションは、for ループの前に複数回指定できます。オプションが衝突を起こす場合には、コンパイラによって警告メッセージが出力されます。
MP taskloop プラグマは、現在のブロック内にある次の for ループに適用されます。MP C によって並列化された for ループに入れ子は存在しません。
MP taskloop プラグマによって、ループを並列化するように指示されます。
不規則なフロー制御や、一定しない増分による繰り返しを持ったループに対しては、正当な並列化を実行できません。たとえば、setjmp、longjmp、exit、abort、return、goto、ラベル、break を含んだ for ループは並列化に適しません。
特に重要なこととして、繰り返し間の依存性を持った for ループでも、明示的に並列化できる点に注意してください。すなわち、このようなループに対して MP taskloop プラグマが指定されていると、for ループが並列化に適していないと判断されない限り、単にこの指示に従ってコンパイルを実行してしまいます。このような明示的な並列化を行なった場合は、不正確な結果が発生しないかを確認してください。
1 つのループに対して serial_loop または serial_loop_nested と taskloop の両方のプラグマが指定されている場合には、最後の指定が優先的に使用されます。
#pragma MP serial_loop_nested for (i=0; i<100; i++) { # pragma MP taskloop for (j=0; j<1000; j++) { ... } }
この例では、i ループは並列化されませんが、j ループは並列化が可能です。
#pragma MP taskloop maxcpus (<プロセッサの数>) は、指定が可能であれば、現在のループに対して使用される CPU の数を指定します。
maxcpus に指定する値は正の整数でなければなりません。maxcpus が 1 であれば、指定されたループは直列に実行されます。なお、maxcpus を 1 に指定した場合には、doserial 指令をしたことと同等になる点に注意してください。また、maxcpus の値か PARALLEL 環境変数のどちらか小さい方の値が使用されます。環境変数 PARALLEL が指定されていない場合には、この値に 1 が指定されているものとして扱われます。
1 つの for ループに複数の maxcpus プラグマが指定されている場合には、最後に指定された値が優先的に使用されます。
ループに使用される変数は、「スレッド固有」、「共有」、「縮約」または「読み取り専用」のどれかに分類されます。1 つの変数は、これらの種類のうち 1 つにのみ属します。 変数の種類を縮約または読み取り専用にするには、明示的にプラグマで指示しなければなりません。詳しくは、「縮約変数」および 「読み取り専用変数」を参照してください。変数を「スレッド固有」または「共有」にするには明示的にプラグマを使用するか、または以下のスコープの規則にもとづいて決まります。
スレッド固有変数は、for ループのある繰り返しを処理するためにそれぞれの CPU が専用に使用する値を保持します。別の言い方をすれば、for ループのある繰り返しでスレッド固有変数に割り当てられた値は、そのループの別の繰り返しを処理している CPU からは見えません。これに対して共有変数は、このような特性を持っていません。共有変数とは、現在のループ繰り返しを処理しているすべての CPU から現在の値にアクセスできる変数のことです。ループのある繰り返しを処理している 1 つの CPU が共有変数に代入した値は、そのループの別の繰り返しを処理している CPU からでも見ることができます。共有変数を参照しているループを #pragma MP taskloop 指令によって明示的に並列化する場合には、値の共有によって正確性に問題が起きないことを確認しなければなりません (競合条件の確認など)。明示的に並列化されたループの共有変数へのアクセスおよび更新では、コンパイラによる同期はとられません。
明示的に並列化されたループの解析において、変数がスレッド固有と共有のどちらであるかを決定するために、以下の「デフォルトのスコープの規則」が使用されます。
変数がプラグマによって明示的に分類されていない場合には、その変数がポインタまたは配列として宣言されていて、かつループ内では配列構文を使用して参照している限り、その変数はデフォルトで共有変数として分類されます。
ループのインデックス変数は常にスレッド固有変数として扱われ、また常に書き戻しの対象として扱われます。
明示的に並列化された for ループ内のすべての変数を、共有、スレッド固有、縮約、または読み取り専用として明示的に指定し、デフォルトのスコープの規則が適用されないようにすることを、強くお勧めします。
コンパイラは、共有変数に対するアクセスの同期を一切実行しないので、たとえば、配列参照を含んだループに対して MP taskloop プラグマを使用する前には、十分な考察が必要になります。このように明示的に並列化されたループで、繰り返し間でのデータ依存性がある場合には、並列実行を行うと正しい結果を得られないことがあります。コンパイラによって、このような潜在的な問題を検出し、警告メッセージを出力することもできますが、一般的にこれを検出することは非常に困難です。なお、共有変数に対する潜在的な問題を持ったループでも、明示的に並列化を指示されると、コンパイラはこの指示に従います。
#pragma MP taskloop private (<スレッド固有変数のリスト>) は、現在のループでスレッド固有変数として扱われる必要のあるすべての変数を指定します。ループで使用されている別の変数は、それ自体が明確に共有、読み取り専用、ストアバック、または縮約であることが指定されていない限り、デフォルトのスコープの規則に従って、共有またはスレッド固有のどちらかに分類されます。
スレッド固有変数とは、それ自体の値がループのある繰り返しを処理する CPU 専用になっている変数のことです。別の言い方をすれば、ループのある繰り返しを処理している CPU によってスレッド固有変数に代入された値は、そのループの別の繰り返しを処理している CPU から見ることはできません。スレッド固有変数には、ループの繰り返しの開始時に初期値は代入されず、繰り返し内で最初に使用される前に、その繰り返し内で値が代入されなければなりません。値が設定される前にその値を参照するように明確に宣言されたスレッド固有変数を持つループを実行すると、その動作は保証されません。
#pragma MP taskloop (<共有変数のリスト>) は、現在のループで共有変数として扱われる必要のあるすべての変数を指定します。ループで使用されている別の変数は、それ自体が明確にスレッド固有、読み取り専用、ストアバック、または縮約であることが指定されていない限り、デフォルトのスコープの規則に従って、共有またはスレッド固有のどちらかに分類されます。
共有変数とは、ある for ループの繰り返しを処理しているすべての CPU から現在の値を見ることのできる変数のことです。ループのある繰り返しを処理している CPU が共有変数に代入した値は、そのループの別の繰り返しを処理している CPU からでも見ることができます。
#pragma MP taskloop readonly (<読み取り専用変数リスト>) は、現在のループで読み取り変数として扱われる必要のあるすべての変数を指定します。
読み取り専用変数とは、共有変数の特殊なクラスのことで、ループのどの繰り返しでも、その値を変更できません。変数を読み取り専用として指定すると、ループの繰り返しを処理しているそれぞれの CPU に対して、個々に変数値のコピーが使用されます。
#pragma MP taskloop storeback (<ストアバック変数リスト>) は、現在のループでストアバック変数として扱われる必要のあるすべての変数を指定します。
ストアバック変数とは、ループの中で変数値が計算され、その値がループの終了後に使用される変数のことです。ループの最後の繰り返しにおけるストアバック変数の値が、ループの終了後に利用可能になります。このような変数は、その変数が明示的な宣言やデフォルトのスコープ規則によってスレッド固有変数となっている場合には、この指令を使用して明示的にストアバック変数として宣言するとよいでしょう。
なお、ストアバック変数に対する最終的な書き戻し操作は、明示的に並列化されたループの最後の繰り返しにおいて、その中で実際に値が変更されたかどうかには関係なく実行される点に注意してください。すなわち、ループの最後の繰り返しを処理する CPU と、ストアバック変数の最終的な値を保持している CPU とは、異なる可能性があります。
#pragma MP taskloop private(x) #pragma MP taskloop storeback(x) for (i=1; i <=n; i++) { if (...) { x =... } } printf ("%d", x);
上述の例では、printf() 呼び出しによって出力されるストアバック変数 x の値は、i のループを直列に実行した場合の出力値とは異なる可能性があります。なぜならば、明示的に並列化されたループでは、ループの最後の繰り返し (すなわち i==n のとき) を処理し、x に対してストアバック操作を行う CPU は、現在最後に更新された x の値を保持する CPU とは同じでないことがあるからです。このような潜在的な問題に対し、コンパイラは警告メッセージを出力します。
明示的に並列化されたループでは、配列として参照される変数をストアバック変数とは扱いません。したがって、このような変数にストアバック処理が必要な場合 (たとえば、配列として参照される変数がスレッド固有変数として宣言されている場合) には、その変数を <ストアバック変数リスト> に含める必要があります。
このプラグマによって、ループ内のすべてのスレッド固有変数がストアバック変数として扱われます。このプラグマの構文を以下に示します。
#pragma MP taskloop savelast
各変数をストアバック変数として宣言するときには、それぞれのスレッド固有変数をリストするよりも、この形式が便利であることがよくあります。
リストにあるすべての変数は、そのループに対しての縮約変数として扱われます。縮約変数とは、ループのある繰り返しを処理している個々の CPU によって、その値を部分的に計算され、最終値がすべての部分値から計算される変数のことをいいます。縮約変数リストにより、そのループが縮約ループであることをコンパイラに示し、適切な並列縮約用のコードを生成できるようにします。以下の例を考えてみましょう。
#pragma MP taskloop reduction(x) for (i=0; i<n; i++) { x = x + a[i]; }
ここでは、変数 x が (和の) 縮約変数であり、i ループが (和の) 縮約ループになっています。
MP C コンパイラには、指定されたループのスケジューリングを戦略的に制御するために、taskloop プラグマと同時に使用するいくつかのプラグマが用意されています。このプラグマの構文を以下に示します。
#pragma MP taskloop schedtype ( <スケジューリング型>)
このプラグマによって、並列化されたループをスケジュールするための <スケジューリング型> を指定することができます。<スケジューリング型> には、以下のいずれかを指定できます。
static
静的スケジューリングでは、ループ内のすべての繰り返しが、そのループを処理するすべての CPU に均等に配分されます。
#pragma MP taskloop maxcpus(4) #pragma MP taskloop schedtype(static) for (i=0; i<1000; i++) { ... }
上述の例では、4 個の CPU が、ループの繰り返しを 250 ずつ処理します。
self [<チャンクサイズ>]
自己スケジューリングでは、ループのすべての繰り返しが処理されるまで、固定された回数の繰り返し <チャンクサイズ> を、そのループを処理するそれぞれの CPU で処理します。<チャンクサイズ> は省略可能で、使用するチャンクサイズを指定します。 <チャンクサイズ> は、正の整数の定数か、もしくは整数型の変数でなければなりません。変数が指定された場合、<チャンクサイズ> はそのループを開始する前に、その変数が正の整数を保持していなければなりません。このオプションの引数が指定されていない場合、もしくは、この値が正の整数ではない場合、チャンクサイズはコンパイラによって決められます。
#pragma MP taskloop maxcpus(4) #pragma MP taskloop schedtype(self(120)) for (i=0; i<1000; i++) { ... }
上述の例では、ループを処理するそれぞれの CPU に割り当てられる繰り返し数は、割り当て順に以下のようになります。
120, 120, 120, 120, 120, 120, 120, 120, 40.
gss [<最小チャンクサイズ>]
ガイド付き自己スケジューリング (GSS) では、ループのすべての繰り返しが処理されるまで、可変な繰り返し回数 (チャンクサイズ) を、そのループを処理するそれぞれの CPU で処理します。オプションの <最小チャンクサイズ> 引数によって、可変チャンクサイズがそれぞれ最低でも <最小チャンクサイズ> になるように指示されます。 <最小チャンクサイズ> は、正の整数の定数か、もしくは整数型の変数でなければなりません。変数が指定された場合、<最小チャンクサイズ> はそのループを開始する前に、その変数が正の整数を保持していなければなりません。このオプションの引数が指定されていない場合、もしくは、この値が正の整数ではない場合には、チャンクサイズはコンパイラによって決められます。
#pragma MP taskloop maxcpus(4) #pragma MP taskloop schedtype(gss(10)) for (i=0; i<1000; i++) { ... }
上述の例では、ループを処理するそれぞれの CPU に割り当てられる繰り返し数は、割り当て順に以下のようになります。
250, 188, 141, 106, 79, 59, 45, 33, 25, 19, 14, 11, 10, 10, 10.
factoring [<最小チャンクサイズ>]
ファクタリング・スケジューリングでは、ループのすべての繰り返しが処理されるまで、可変数の繰り返し (チャンクサイズ) を、そのループを処理するそれぞれの CPU で処理します。オプションの <最小チャンクサイズ> 引数によって、可変チャンクサイズが、それぞれ最低でも <最小チャンクサイズ> になるように指示されます。<最小チャンクサイズ> は、正の整数の定数か、もしくは整数型の変数でなければなりません。変数が指定された場合、<最小チャンクサイズ> はそのループを開始する前に、その変数が正の整数を保持していなければなりません。このオプションの引数が指定されていない場合、もしくは、この値が正の整数ではない場合には、チャンクサイズはコンパイラによって決められます。
#pragma MP taskloop maxcpus(4) #pragma MP taskloop schedtype(factoring(10)) for (i=0; i<1000; i++) { ... }
上述の例では、ループを処理するそれぞれの CPU に割り当てられる繰り返し数は、割り当て順に解釈すると以下のようになります。
125, 125, 125, 125, 62, 62, 62, 62, 32, 32, 32, 32, 16, 16, 16, 16, 10, 10, 10, 10, 10, 10.
MP C では次のコンパイラオプションを使用することができます。これらのオプションの詳細は、第 2 章「cc コンパイラオプション」で説明してあります。
-xautopar (「-xautopar 」)
-xdepend (「-xdepend」)
-xexplicitpar (「-xexplicitpar」)
-xloopinfo (「-xloopinfo」)
-xparallel (「-xparallel」)
-xreduction (「-xreduction」)
-xrestrict = [<関数 1>, ... ,<関数 n>] (「-xrestrict=f 」)
-xvpara (「-xvpara」)
-Zlp (「-Zlp」)