Fortran プログラミングガイド |
第 10 章
SPARC: 並列化
この章では、マルチプロセッサの並列化の概要を示し、サンの Fortran コンパイラの機能について説明します。
f77
とf95
との実装の違いについても述べます。
注 - Fortran 並列化機能を使用するには、Sun WorkShop HPC ライセンスが必要です。
基本概念
アプリケーションの並列化 (またはマルチスレッド化) とは、マルチプロセッサシステム上で実行できるよう、またはマルチスレッド環境、コンパイルされたプログラムを分散することです。並列化によって、1 つのタスク (
DO
ループなど) を複数のプロセッサ (またはスレッド) を使って実行できるので、実行速度が上がる可能性があります。UltraTM 60、EnterpriseTM Server 6500、または Sun Enterprise Server 10000 のようなマルチプロセッサシステム上でアプリケーションプログラムを効率的に実行できるようにするためには、そのアプリケーションプログラムをマルチスレッド化する必要があります。つまり、並列実行できるタスクを識別し、複数のプロセッサまたはスレッドを横にしてその計算を分配するようにプログラムを変更する必要があります。
アプリケーションのマルチスレッド化は、
libthread
プリミティブを適切に呼び出すことによって、手作業で行うことができます。しかし、膨大な量の解析とプログラムの変更が必要となります。詳細は、Solaris の『マルチスレッドのプログラミング』を参照してください。Sun コンパイラは、マルチプロセッサシステム上で動作できるようにマルチスレッド化されたオブジェクトコードを自動的に生成できます。Fortran コンパイラは、並列性をサポートする主要な言語要素としての
DO
ループに焦点をあわせます。並列化は、Fortran ソースプログラムに一切手を加えることなく、ループの計算作業を複数のプロセッサに分配します。どのループを並列化するか、またそのループをどのように分配するかは、完全にコンパイラに任せることも (
-autopar
)、ソースコード指令を使用してプログラマが明示的に決定することも (-explicitpar
)、その両方を組み合わせることも (-parallel
) できます。
注 - 独自の (明示的な) スレッド管理を行うプログラムをコンパイルするときは、コンパイラのどのような並列化オプションも付けてはなりません。明示的なマルチスレッド化 (libthread
プリミティブへの呼び出し) は、並列化オプションを付けてコンパイルしたルーチンと組み合わせることはできません。
プログラム中のすべてのループが有効に並列化されるわけではありません。計算作業量の少ないループを並列化すると、(並列タスクの起動と同期に費やされるオーバーヘッドと比べると) 実際には実行が遅くなることもあります。また、安全に並列化できないループもあります。このようなループは、文間あるいは反復間の依存関係のため、並列化すると異なる結果を生成します。
明示的なDO
ループとともに暗示的なループ (IF
ループと Fortran 95 配列構文など)が、Fortran コンパイラでの自動並列化の対象となります。
Sun WorkShop コンパイラは、安全にそして有効に並列化できる可能性のあるループを自動的に検出できます。しかし、ほとんどの場合、隠れた副作用の恐れがあるので、この解析はどうしても控え目になります (どのループが並列化され、どのループが並列化されていないかは、
-loopinfo
オプションで表示できます)。ループの前にソースコード指令を挿入することによって、特定のループを並列化するかどうかを明示的に制御できます。しかし、このように明示的に並列化を指定したループによって結果が間違ったとしても、それはユーザーの責任になります。
f77
もf95
も、Sun 形式と Cray 形式の 2 つの明示的並列化指令をサポートしています。さらに、f95
は OpenMP 1.1 指令や実行時ライブラリルーチンをサポートしています。Fortran の明示的並列化機能については、187ページで説明します。速度向上 -- 何を期待するか
4 つのプロセッサ上で動作するようにプログラムを並列化した場合、そのプログラムは、1 つのプロセッサ上で動作させるときの約 1/4 の時間で処理できる (4 倍の速度向上になる) と期待できるでしょうか。
おそらく、答えは「ノー」です。プログラムの全体的な速度向上は、並列実行しているコード中で消費される実行時間の割り合いによって厳密に制限されると証明できます (アムダールの法則)。適用されるプロセッサがいくつになろうとも、これは常に真です。事実、並列実行した実行プログラムの合計時間のパーセンテージを p とすると、理論的な速度向上の制限は 100/(100-c) となります。したがって、プログラムの 60% だけが並列実行した場合、プロセッサの数にかかわらず、速度向上は最大 2.5 倍です。そして、プロセッサが 4 つの場合、このプログラムの理論的な速度向上は、最大限の効率が発揮されたと仮定しても、1.8 倍です。4 倍にはなりません。オーバーヘッドを考えると、実際の速度向上はもう少し減ります。
最適化のことを考えると、ループの選択は重要です。プログラムの合計実行時間のほんの一部としか関わらないループを並列化しても、最小の効果しか得られません。効果を得るためには、実行時間の大部分を消費するループを並列化しなければなりません。したがって、どのループが重要であるかを決定し、そこから始めるのが第一歩です。
問題のサイズも、並列実行するプログラムの割合を決定するのに重要な役割を果たし、その結果、速度向上にもつながります。問題のサイズを増やすと、ループの中で行われる作業量も増えます。3 重に入れ子にされたループは、作業量が 3 乗になる可能性があります。入れ子の外側のループを並列化する場合、問題のサイズを少し増やすと、(並列化していないときのパフォーマンスと比べて) パフォーマンスが大幅に向上します。
プログラムの並列化のための手順
次に、アプリケーションの並列化に必要な手順について、極めて一般的な概要を示します。
1. 最適化。適切なコンパイラオプションのセットを使用して、1 つのプロセッサ上で最高のパフォーマンスを得ます。2. プロファイル。典型的なテストデータを使用して、プログラムのパフォーマンスプロファイルを決定します。最も重要なループを見つけます。3. ベンチマーク。逐次処理でのテストの結果が正確かどうかを決定します。これらの結果とパフォーマンスプロファイルをベンチマークとして使用します。4. 並列化。オプションと指令の組み合わせを使用して、並列化した実行可能ファイルをコンパイルし、構築します。5. 検証。並列化したプログラムを 1つのプロセッサや 1 つのスレッド上で実行し、結果を検査して、その中の不安定さやプログラミングエラーを見つけます ($PARALLEL
または$OMB_NUM_THREADS
に 1 を設定します。178ページを参照してください) 。6. テスト。複数のプロセッサ上でさまざまな実行を試し、結果を検査します。7. ベンチマーク。専用のシステムで、プロセッサの数を変えながらパフォーマンスを測定します。問題のサイズを変化させて、性能の変化を測定します (スケーラビリティ)。8. 手順 4 から 手順 7 を繰り返す。パフォーマンスに基づいて、並列化スキームを改良します。データ依存性の問題
すべてのループが並列化できるわけではありません。複数のプロセッサ上でループを並列実行すると、実行している反復の順序が変わる可能性があります。さらに、ループを並列実行する複数のプロセッサがお互いに干渉する可能性もあります。このような状況が発生するのは、ループ中にデータ依存性がある場合です。
データ依存性の問題が発生する場合は、再帰、縮約、間接アドレス指定、データに依存するループが繰り返されています。
再帰
ループのある反復で設定され、後続の反復で使用される変数は、反復間依存性、つまり再帰の原因となります。ループ中で再帰を行う場合は、反復が適切な順序で実行されなければなりません。
DO I=2,NA(I) = A(I-1)*B(I)+C(I)END DOたとえば、上記コードでは、以前の反復中で
A(I)
用に計算された値が、現在の反復中で (A(I-1)
として) 使用されなければなりません。各反復を並列実行して、1 つのプロセッサで実行したときと同じ結果を生成するためには、反復I
は、反復I+1
が実行できる前に完了していなければなりません。縮約
縮約操作は、配列の要素を 1 つの値に縮約します。たとえば、配列の要素の合計を 1 つの変数にまとめる場合、その変数は反復ごとに更新されます。
DO K = 1,NSUM = SUM + A(I)*B(I)END DOこのループを並列実行する各プロセッサが反復のサブセットを取る場合、
SUM
の値を上書きしようとして、各プロセッサはお互いに干渉します。うまく処理するためには、各プロセッサが 1 度に 1 回ずつ合計を実行しなければなりません。しかし、順序は問題になりません。ある共通の縮約操作は、コンパイラによって、特別なケースであると認識され、処理されます。
間接アドレス指定
ループ依存性は、値が未知である添字によってループの中の添字付けられた配列への格納から発生する可能性があります。たとえば、添字付の配列中に繰り返される値がある場合、間接アドレス指定は順序に依存することがあります。
DO L = 1,NWA(ID(L)) = A(L) + B(L)END DO上記例中、
ID
中で繰り返される値は、A
の要素を上書きする原因となります。逐次処理の場合、最後の格納が最終値です。並列処理の場合、順序は決定されていません。使用される A(L) の値 (古い値か更新された値) は、順序に依存します。データに依存するループ
並列化できるように、ループを書き換えて、データ依存性を取り除くことができます。しかし、大規模な構造の変形が必要になります。
以上が、並列化のための一般的な条件です。コンパイラの自動並列化解析は、さらに、ループを並列化するかどうかを決定するための基準も考慮します。しかし、指令を使用して、明示的にループを並列化させることも可能です。ループの中に並列化の抑制要因が入っていたり、ループが間違った結果を生成する場合でも可能です。
並列オプションと指令についての要約
次の表に、Sun WorkShop 6
f77
とf95
の並列化に関するコンパイルオプションを示します。
-reduction
を指定するときは-autopar
も必要です。-autopar
には-depend
とループ構造の最適化が含まれます。-parallel
は-autopar
-explicitpar
と同義です。- 打ち消しのオプションには、
-noautopar、-noexplicitpar、-noreduction
があります。- 並列化オプションはどのような順序で指定してもかまいません。しかし、必ずすべてを小文字にしなければなりません。
- 明示的に並列化されたループに対して、縮約操作は解析されません。
- いずれの並列化オプションを使用する場合にも、WorkShop のライセンスが必要です。
-openmp
はオプション組み合わせのマクロです。-mp=openmp -stackvar -explicitpar
- オプション
-loopinfo
、-vpara
、-mp
は、並列化オプション-autopar
、-explicitpar
、-parallel
のいずれかとともに使用しなければなりません。
表 10-2 並列化指令 C$PAR TASKCOMMON
各スレッドの共通ブロックを非公開として宣言 C$PAR DOALL
オプションの修飾子可能であれば、次のループを並列化 C$PAR DOSERIAL
次のループの並列化を抑制 C$PAR DOSERIAL *
ループの入れ子の並列化を抑制
Cray 形式の指令は似ていますが (206ページを参照してください)、
C$PAR
の変わりにCMIC$
を使用し、またDOALL
指令で別のオプション修飾子を使用します。これらの指令の使用法については、「明示的な並列化」で説明します。また、『Fortran ユーザーズガイド』の付録 E に、これらの指令や Fortran 95 OpenMP を含め、すべての Fortran 指令の詳しい要約を示します。スレッドの数
PARALLEL
(またはOMP_NUM_THREADS
) 環境変数は、プログラムで使用可能なスレッドの最大数を制御します。環境変数を設定することにより、実行時システムに、プログラムで使用可能なスレッドの最大数が知らされます。デフォルトは 1 です。一般に、PARALLEL
変数またはOMP_NUM_THREADS
変数に、ターゲットプラットフォームで使用可能なプロセッサ数を設定します。
demo%setenv PARALLEL 4
C シェルまたはdemo%PARALLEL=4
Bourne/Korn シェルdemo%export PARALLEL 4
上記例では、
PARALLEL
を
4
に設定することで、
プログラムの実行は最大 4 つのスレッドを使用できます。ターゲットマシンが 4 つのプロセッサを利用できる場合、各スレッドはプロセッサ 1 つずつにマップされます。利用可能なプロセッサが 4 つより少ない場合、スレッドのいくつかは他のスレッドと同じプロセッサ上で実行されるので、パフォーマンスは下がります。SunOS コマンド
psrinfo
(1M) は、システムで利用可能なプロセッサのリストを表示します。
demo%psrinfo
0 on-line since 03/18/96 15:51:031 on-line since 03/18/96 15:51:032 on-line since 03/18/96 15:51:033 on-line since 03/18/96 15:51:03スタック、スタックサイズ、並列化
プログラムの実行は、プログラムを最初に実行したスレッドのためにメインメモリーのスタックを保持し、各ヘルパースレッドのために個々のスタックを保持します。スタックとは、副プログラムの呼び出し時に引数と
AUTOMATIC
変数を保持するために使用される一時的なメモリーアドレス空間です。メインスタックのデフォルトのサイズは、約 8M バイトです。Fortran コンパイラは、通常、局所変数と配列を (スタックにではなく)
STATIC
として割り当てます。しかし、-stackvar
オプションを使用すると、すべての局所変数と配列をスタックに割り当てます (あたかもそれがAUTOMATIC
変数であるかのように)。-stackvar
は並列化とともに使用することを推奨します。なぜなら、ループ中のCALL
を並列化するオプティマイザの能力を向上させるからです。-stackvar
は、副プログラム呼び出しを持つ明示的に並列化されたループには必須です。-stackvar
については、『Fortran ユーザーズガイド』を参照してください。C シェル (
csh
) を使用し、limit
コマンドにより現在のメインスタックのサイズを表示し、設定します。
ulimit
です。マルチスレッド化されたプログラムの各スレッドは、独自のスレッドスタックを持っています。このスタックは、初期スレッドのスタックと似ています。しかし、スレッド固有のものです。スレッドの
PRIVATE
配列と変数 (スレッドに局所的な) は、スレッドスタックに割り当てられます。デフォルトのサイズは 2 メガバイトです。このサイズは、STACKSIZE
環境変数で設定されます。
demo%setenv STACKSIZE 8192
<- スレッドスタックサイズを
8M バイトに設定 C シェルまたはdemo%STACKSIZE=8192
Bourne/Korn シェルdemo%export STACKSIZE 8192
いくつかの並列化された Fortran コードに対しては、スレッドスタックのサイズをデフォルトより大きく設定することが必要になります。しかし、どれくらいの大きさに設定すればいいのかを知る方法はなく、試行錯誤してみるしかありません。特に、専用配列または局所配列が関連する場合はわかりません。スタックのサイズが小さすぎてスレッドが実行できない場合、プログラムはセグメンテーションフォルトで異常終了します。
自動並列化
-autopar
オプション と-parallel
オプションを使用すると、f77
およびf95
コンパイラは、効率的に並列化できるDO
ループを自動的に見つけます。このようなループは変形され、利用可能なプロセッサに対してその反復が均等に分配されます。コンパイラは、このために必要なスレッド呼び出しを生成します。ループの並列化
コンパイラによる依存性の解析は、
DO
ループを並列化可能なタスクに変形します。コンパイラは、ループの構造を変形して、逐次実行する、並列化できないセクションを切り離します。次に、利用可能なプロセッサに対して作業を均等に分配します。各プロセッサが反復の異なったブロックを実行します。たとえば、4 つの CPU と 1,000 回の反復を持つ並列化ループの例で、各スレッドは 250 回の反復をまとめて実行します。
プロセッサ 2 が実行する反復 251 から 500 プロセッサ 3 が実行する反復 501 から 750 プロセッサ 4 が実行する反復 751 から 1000
並列化できるのは、計算の実行順序に依存しないループだけです。コンパイラによる依存性の解析は、本質的にデータ依存性をもつループを拒否します。ループ中のデータフローを完全に決定できない場合、コンパイラは保守的に動作し、並列化を行いません。また、パフォーマンスの向上よりもオーバーヘッドが勝る場合、ループを並列化しないことを選択する可能性もあります。
コンパイラは常に、静的ループスケジューリング (つまり、ループ中の作業を単純に均等な反復ブロックに分割する方法) を使用して、ループを並列化することを選択することに注意してください。明示的な並列化指令を使用すれば、他の分配スキームも指定できます。この指令については、この章の後半で説明します。
配列、スカラー、純スカラー
純スカラーとは、別名付けされていない (
EQUIVALENCE
文やPOINTER
文で参照されていない) スカラー変数のことです。
dimension a(10)real m(100,10), s, u, x, zequivalence ( u, z )pointer ( px, x )s = 0.0...変数
u、x、z、px
はスカラー変数ですが、純スカラーではありません。自動並列化の基準
反復間データ依存性をもたない
DO
ループは、-autopar
か-parallel
によって自動的に並列化されます。自動並列化のための一般的な基準は次のとおりです。
- 明示的な
DO
ループと、IF
ループや Fortran 95 配列構文などの暗黙的なループのみが、並列化されます。- ループの各反復に対する配列変数の値は、そのループの他の反復に対する配列変数の値に依存してはなりません。
- ループ内の計算は、ループの終了後に参照される純スカラー変数を条件によって変更してはなりません。
- ループ内の計算は、反復にまたがるスカラー変数を変更してはなりません。これは「ループ伝達の依存性」と呼ばれます。
- ループの本文内の処理量は、並列化のオーバーヘッドよりも多くなければなりません。
f77: 見かけの依存性
f77
コンパイラは、コンパイルされたコードを変形するときに、依存の原因になりそうな (見かけの) 参照を自動的に取り除きます。このような多数の変換の 1 つは、一部の配列の専用バージョンを使用します。コンパイラがこの処理を行うことができるのは、一般的には、そのような配列が本来のループで一時領域としてのみ使用されていることが判断できる場合です。例 :
-autopar
を使用しています。専用配列によって依存が取り除かれます。
parameter (n=1000)real a(n), b(n), c(n,n)do i = 1, 1000 <-- 並列化されるdo k = 1, na(k) = b(k) + 2.0
end dodo j = 1, nc(i,j) = a(j) + 2.3
end doend doend上記の例では、外側のループが並列化され、別々のプロセッサ上で実行されます。配列
a
を参照する内側のループはデータ依存性の原因になるように見えますが、コンパイラはその配列の一時的な専用コピーを作成して、外側のループの反復を依存しないようにしています。自動並列化の抑制要因
自動並列化では、次のいずれかが発生すると、コンパイラはループを並列化しません。
DO
ループが、並列化される別のループ内の入れ子になっているとき- フロー制御で、
DO
ループの外に飛び出す可能性があるとき- ループ内で、ユーザーレベルの副プログラムが起動されているとき
- ループ内に入出力文があるとき
- ループ内の計算が別名付きスカラー変数を変更するとき
入れ子にされたループ
マルチプロセッサシステムでは、最も内側のループではなく、ループの入れ子の最も外側のループを並列化するのが最も効果的です。並列処理は一般にループのオーバーヘッドがかなり大きいため、最も外側のループを並列化することでループのオーバーヘッドが最小になり、各プロセッサの処理量が最大になります。自動並列化では、コンパイラは入れ子の最も外側のループからループの解析を始め、並列化可能なループが見つかるまで、内側に進んでいきます。入れ子の中でループが 1 つでも並列化されたら、並列ループの中に含まれるループは無視されます。
縮約操作を使用した自動並列化
配列をスカラーに変形する計算のことを「縮約操作」と呼びます。典型的な縮約操作は、ベクトルの要素の合計や積です。縮約操作は、ループ内の計算が反復にまたがって累積的に変数を変更しないという基準には反するものです。
s = 0.0do i = 1, 1000s = s + v(i)end dot(k) = sしかし、一部の操作では、並列化を妨げるのが縮約だけの場合は、この基準にかかわらず並列化できます。共通の縮約操作が頻繁に発生するので、コンパイラはこれらの操作を特別なケースであると認識し、並列化します。
-reduction
コンパイラオプションが-autopar
か-parallel
とともに指定されていなければ、縮約操作の認識は、自動並列化解析の中には含まれません。並列化可能なループが表 10-3 にリストされた縮約操作のいずれか 1 つを持つ場合、
-reduction
が指定されていれば、コンパイラはそのループを並列化します。認識される縮約操作
次の表に、
f77
およびf95
が認識する縮約操作をリストします。
数値的な正確性と縮約操作
次の条件のため、浮動小数点の合計や積の縮約操作が不正確になることがあります。
- 計算が並列実行されるときの順序が、1 つのプロセッサ上で逐次実行されるときの順序と違う場合
- 計算の順序が、浮動小数点数の合計や積に影響を与えた場合。ハードウェア浮動小数点の加算や乗算は結合則を満たしません。どのように演算対象が関連付られているかによって、丸め、オーバーフロー、アンダーフローが発生する可能性があります。たとえば、
(X*Y)*Z
とX*(Y*Z)
は、数値的には意味が違う可能性があります。例 : 縮約の並列化を指定したときと指定しないときの、オーバーフローとアンダーフローです。
例 : 丸めの例です。-1 と +1 の間の 100,000 個の乱数を合計します。
結果は、プロセッサの数によって異なります。次の表に、-1 と +1 の間の 100,000 個の乱数の合計を示します。
1 s = 0.568582080884714E+022 s = 0.568582080884722E+023 s = 0.568582080884721E+024 s = 0.568582080884724E+02
この状況では、丸めの誤差はおよそ 10-14 なので、この乱数のデータは容認できます。詳細は、『数値計算ガイド』を参照してください。
明示的な並列化
この節では、どのループを並列化するか、どの方針を使用するかを明示的に指示するための、
f77
とf95
によって認識されるソースコード指令について説明します。Sun WorkShop 6 Fortran コンパイラは、Sun 形式と Cray 形式の並列化指令を受け付けるため、明示的に並列化されたプログラムを他のプラットフォームから移植しやすくなっています。
Fortran 95 コンパイラは、OpenMP の Fortran 並列化指令も受け付けます。OpenMP の Fortran 仕様は、
http://www.openmp.org
にあります。OpenMP 指令、ライブラリルーチン、環境変数については、『Fortran ユーザーズガイド』の付録 E に要約しています。プログラムを明示的に並列化するためには、アプリケーションコードの事前解析と深い理解、そして、共有メモリー並列化の概念が必要です。
DO
ループに並列化のためのマークを付けるには、ループの直前に指令を置きます。DO
ループを認識させ、並列コードを生成させるためには、コンパイラオプション-parallel
と-explicitpar
を使用しなければなりません。並列化指令は、指令後のDO
ループを並列化する (または並列化しない) ようにコンパイラに知らせるコメント行です。指令は、プラグマともいいます。どのループに並列化のマークを付けるかを選択するときは注意してください。並列実行するときに間違った結果を計算してしまうデータ依存性がループにある場合でも、コンパイラは、
DOALL
指令でマークを付けられたすべてのループに対して、スレッド化された並列コードを生成します。
libthread
プリミティブを使用して独自のマルチスレッド化コーディングを行う場合は、コンパイラのいかなる並列化オプションも付けてはなりません。コンパイラは、すでにスレッドライブラリへのユーザーの呼び出しを使用して並列化されたコードを並列化できません。並列可能なループ
DO
ループであって、DO WHILE
または Fortran 95 の配列構文ではない場合- ループの各反復に対する配列変数の値が、そのループの他の反復に対する配列変数の値に依存しない場合
- ループがスカラーを変更する場合、そのスカラーがループ終了後に参照されない場合。このようなスカラー変数は、ループ終了後定義された値をもつとは保証されません。なぜなら、コンパイラはこのような変数に対しては適切な書き戻しを自動的に行わないからです。
- 各反復において、ループの内側から呼び出される副プログラムが、他の反復に対する配列変数の値を参照しない、または変更しない場合。
DO
ループの添字が必ず整数である場合。スコープ規則: 非公開と共有
非公開変数または専用配列は、ループの 1 回の反復だけで使用されます。ある反復で非公開変数または非公開配列に代入された値は、そのループの別の反復には伝達されません。
共有変数または共有配列は、他のすべての反復で共有されます。ある反復で共有変数または共有配列に代入された値は、そのループの別の反復からも参照されます。
明示的に並列化されたループで共有の値を参照する場合、共有によって正確性の問題が発生しないように注意してください。共有変数が更新またはアクセスされたとき、コンパイラは同期処理を行いません。
あるループの中で変数が非公開であると指定した場合、さらに、その変数の唯一の初期化が他のループの中にある場合、その変数の値はループの中で未定義のままとなる可能性があります。
ループでのサブプログラム呼び出し
ループで (または呼び出し元ルーチン内から呼び出されたサブプログラムで) サブプログラムを呼び出すと、データ依存性が生じる可能性があり、これは呼び出しのチェーンをたどってデータや制御フローを深く分析しなければ気づかないでしょう。作業量の多い一番外側のループを並列化すればよいのですが、これらはサブプログラムをいくつも呼び出してループがとても深くなっている傾向があります。
このような手続き間の分析は難しく、またコンパイル時間がかなり長くなってしまうので、自動並列化モードでは行われません。明示的な並列化では、コンパイラは、DOALL 指令によりマークしたループ内にサブプログラムへの呼び出しが含まれていても、そのループの並列化コードを生成します。この場合も、ループ内に、また呼び出し先サブプログラムを含めてループ内のすべてにおいてデータ依存が存在しないようにすることはプログラマの仕事です。
さまざまなスレッドから 1 つのルーチンを何度も起動すると、局所静的変数への参照でお互いに干渉し合うような問題が発生することがあります。ルーチン内のすべての局所変数を静的変数ではなく自動変数にすることで、この問題は防ぐことができます。このようにしてサブプログラムを起動すると、そのたびに局所変数が固有の領域に保存され、それらがスタック上で保守されるので、何度起動してもお互いに干渉することはなくなります。
局所サブプログラム変数は、自動変数にすることが可能で、
AUTOMATIC
文で指定するか、または-stackvar
オプションを指定してサブプログラムをコンパイルすることでスタック上に常駐させることができます。ただし、DATA
文で初期化された局所変数については、実際の割り当てで初期化されるように書きかえる必要があります。
注 - 局所変数をスタックに割り当てると、スタックがオーバーフローしてしまう可能性があります。スタックのサイズを大きくする方法については、「スタック、スタックサイズ、並列化」を参照してください。
明示的並列化の抑制
一般に、ユーザーがコンパイラにループを並列化するように明示的に指示している場合、コンパイラはそのようにします。ただし、例外もあり、ループによってはコンパイラが並列化を行わないものがあります。
次に、
DO
ループの明示的な並列化を妨げる抑制の中で、検出可能なものを示します。
DO
ループが、並列化された別のDO
ループ内にネストされている場合。
- この例外は、間接ネストについても当てはまります。ユーザーがサブルーチンを呼び出しているループを明示的に並列化すると、コンパイラにそのサブルーチン内のループを並列化するように要求しても、これらのループは実行時に並列で実行されません。
- フロー制御文により、
DO
ループから外部へのジャンプが許可されている場合。- ループの添字変数が、等価になるなどの影響を受ける場合。
-vpara
を指定してコンパイルすると、コンパイラが明示的にループを並列化している最中に問題を検出すると診断メッセージが発せられます。この場合、コンパイラはループを並列化できます。次に、一般にコンパイラにより検出される並列化の問題を示します。
...
C$PAR DOALL
do 900 i = 1, 1000 !
並列化します(外側のループ)do 200 j = 1, 1000 !
並列化しません、警告も発しません...
200 continue900 continue...
この例では、サブルーチン自体が並列で実行されているので、その中のループは並列化されません。
C$PAR DOALL
do i = 1, 1000 !
並列化されません、警告が発せられます
...if (a(i) .gt. min_threshold ) go to 20...end do20 continue...
C$PAR DOALL
do 100 i = 1, 200 !
並列化されます、警告が発せられますy = y * i !
y にはループ繰越依存がありますa(i) = y100 continue...明示的並列化での入出力
並列に実行するループで入出力を実行できます。ただし、次の条件があります。
この例で、プログラムは
libF77_mt
でデッドロックとなり、停止してしまう可能性があります。キーボード制御を取り戻すには Ctrl+C キーを押します。並列化されたループ内で入出力が発生しても、プログラマがそれに気づかない場合があります。たとえば、算術例外 (ゼロによる除算など) が発生したときに出力を印刷する例外ハンドラをユーザーが指定していたとします。並列化されたループで例外が発生すると、ハンドラからの暗黙の入出力が原因で入出力のデッドロックが発生し、システムが停止してしまう可能性があります。
- ライブラリ
libF77_mt
は、MT 安全 (マルチスレッドで使用しても安全) ですが、ほとんどは MT 活用ではありません。-mt
を指定してコンパイルした場合、再帰的な (ネストされた) 入出力は実行できません。非公式の定義ですが、次の場合、インターフェイスは MT 安全です。
データレースは、メモリのアドレスの内容が複数のスレッドにより更新されようとしており、またそのアドレスがロックにより保護されていない場合に発生します。そのため、そのメモリアドレスの値は不確定となります。2 つのスレッドがスレッドを更新しようとして競合します (ただし、この場合後からスレッドにたどり着いた方が勝者となります)。
マルチスレッド法を使用してパフォーマンスがよくなるように実装が調整されている場合、インタフェースは通常 MT 活用と呼ばれます。詳細については、Solaris の「マルチスレッドのプログラミング」を参照してください。
Sun 形式の並列化指令
Sun 形式の指令は、
-explicitpar
オプションや-parallel
オプションを指定してコンパイルした場合に、デフォルトで (または-mp=sun
オプションを指定して) 使用できます。Sun 並列化指令の構文
並列化指令は、1 つまたは複数の指令行で構成されます。Sun 形式の指令行は次のように定義されます。
C$PAR Directive [ Qualifiers ] <- 初期指令行C$PAR& [More_Qualifiers] <- オプションの継続行
- 指令行は、大文字と小文字の区別がありません。
- 指令行の最初の 5 文字は、
C$PAR
、*$PAR
、!$PAR
のいずれかです。f77
とf95
の固定フォーマットの場合f95
の自由形式の場合- 修飾子がある場合、指令と同じ行または継続行の指令の後に指定します。
- 1 行に複数の修飾子を指定する場合、コンマで区切ります。
- 指令や修飾子の前後、またはその間にある空白は無視されます。
TASKCOMMONCOMMON ブロックの変数をスレッド非公開として宣言する。 DOALL
次のループを並列化する。 DOSERIAL
次のループを並列化しない。 DOSERIAL*
次のループの入れ子を並列化しない。
TASKCOMMON
指令
TASKCOMMON
指令は、グローバルな COMMON ブロックの変数をスレッド非公開として宣言します。共通ブロックで宣言した変数はすべてスレッドに対して非公開変数になりますが、スレッド内ではグローバルなままです。指定した COMMON ブロックだけがTASKCOMMON
として宣言できます。
C$PAR TASKCOMMON
comon_block_name指令は、その指定されたブロックの COMMON 宣言の直前または直後に指定しなければなりません。
この指令が有効になるのは、
-explicitpar
または-parallel
オプションを付けてコンパイルしたときだけです。それ以外の場合では、この指令は無視され、ブロックは通常の共通ブロックとして扱われます。
TASKCOMMON
ブロックで宣言した変数は、すべてのDOALL
ループや、DOALL
ループ内から呼び出されているルーチンでスレッド非公開変数として処理されます。各スレッドはそれぞれ COMMON ブロックのコピーを取得するので、あるスレッドにより書き込まれたデータはその他のスレッドから直接参照することはできません。プログラムの連続部分では、最初のスレッドの COMMON ブロックコピーがアクセスされます。
TASKCOMMON
ブロックの変数は、PRIVATE
、SHARED
、READONLY
などのDOALL
修飾子では使用されません。ブロックが定義されているコンパイルユニットのうちすべてではないが、いくつかでは、共通ブロックをタスク共通として宣言するとエラーになります。
-commonchk=yes
フラグを付けてプログラムをコンパイルすることで、タスク共通整合性の実行時検査を行うことができます。(実行時検査は、パフォーマンスを下げることのできるプログラム開発の段階だけで行ってください。)
DOALL
指令DOALL 指令は、コンパイラにその直後に続く DO ループを並列化するコードを生成するように要求します (
-parallel
オプションまたは-explicitpar
オプションを指定してコンパイルした場合)。
注 - ループが明示的に並列化されている場合、そのループ内の縮約操作の解析と変形は行われません。
demo%cat t4.f
...C$PAR DOALL
do i = 1, na(i) = b(i) * c(i)end dodo k = 1, mx(k) = x(k) * z(k,k)end do...demo%f77 -explicitpar t4.f
DOALL
の修飾子
DOALL
指令のすべての修飾子はオプションです。表 10-4 にそれらを要約します。
PRIVATE
(varlist)
PRIVATE
(varlist) 修飾子は、変数リスト varlist 中のすべてのスカラーと配列がDOALL
ループの非公開であることを指定します。配列とスカラーは両方とも非公開として指定できます。配列の場合、DOALL
ループのスレッドごとに配列全体のコピーが作成されます。DOALL
ループで参照されるスカラーや配列のうち、変数リストに含まれないものはすべて、デフォルトのスコープ規則に従います (188ページ参照)。
C$PAR DOALL PRIVATE(a)do i = 1, na(1) = b(i)do j = 2, na(j) = a(j-1) + b(j) * c(j)end dox(i) = f(a)end do
SHARED
(varlist)
SHARED
(varlist) 修飾子は、変数リスト varlist 中のすべてのスカラーと配列がDOALL
ループにおいて共有されることを指定します。配列とスカラーは両方とも共有として指定できます。共有スカラーと共有配列は、DOALL
ループのすべての反復で共通です。DOALL
ループで参照されるスカラーや配列のうち、変数リストに含まれないものはすべて、デフォルトのスコープ規則に従います。
equivalence (a(1),y)C$PAR DOALL SHARED(y)do i = 1,na(i) = yend do上記の例では、変数
y
は、その値がi
ループの反復間で共有される変数であると指定されています。
READONLY
(varlist)
READONLY
(varlist) 修飾子は、変数リスト varlist 中のすべてのスカラーと配列がDOALL
ループにおいて読み取り専用であることを指定します。読み取り専用のスカラーと配列は、DOALL
ループ中のどの反復においても変更されないという、共有スカラーと共有配列の特別なクラスです。スカラーや配列をREADONLY
として指定すると、コンパイラは、DOALL
ループの各スレッドごとに、その変数または配列の別々のコピーを使用する必要がないということを、コンパイラに示します。
x = 3C$PAR DOALL SHARED(x),READONLY(x)do i = 1, nb(i) = x + 1end do上記の例では、
x
は共有変数です。しかし、READONLY
が指定されているので、コンパイラは、x
の値がI
ループの反復においても変更されないことを信頼できます。
STOREBACK
(varlist)
STOREBACK
変数またはSTOREBACK
配列とは、その値がDOALL
ループで計算される変数または配列のことです。計算された値は、そのループの終了後に使用できます。言い換えると、ループの最後の反復におけるSTOREBACK
スカラーとSTOREBACK
配列の値は、DOALL
ループの外から参照できます。例 : ループインデックス変数を
STOREBACK
として指定します。
C$PAR DOALL PRIVATE(x), STOREBACK(x,i)do i = 1, nx = ...end do... = i... = x上記の例では、変数
x
とi
は両方ともi
ループの非公開変数であり、STOREBACK
変数でもあります。x
値が最後の反復が終わった時点の値であるのに対し、ループの後のi
値はn+1
となります。
STOREBACK
には、留意すべきいくつかの潜在的な問題があります。最後の反復が、
STOREBACK
変数またはSTOREBACK
配列の値を最後に更新する反復と同じ場合でも、STOREBACK
操作は明示的に並列化されたループの最後の反復時に発生します。例 :
STOREBACK
変数は、逐次バージョンとは異なる可能性があります。
C$PAR DOALL PRIVATE(x), STOREBACK(x)do i = 1, nif (...) thenx = ...end ifend doprint *,x上記の例では、出力される
STOREBACK
変数x
の値は、i
ループの逐次バージョンで出力された結果と異なる可能性があります。明示的に並列化された場合、i
ループの最後の反復(i = n
) を処理し、x
のSTOREBACK
操作を行うプロセッサは、現在x
の最後に更新された値をもっているプロセッサとは異なる可能性があります。コンパイラはこのような潜在的な問題に関する警告メッセージを出します。
SAVELAST
SAVELAST
修飾子は、非公開スカラーと非公開配列のすべてがDOALL
ループにおいてSTOREBACK
であることを指定します
C$PAR DOALL PRIVATE(x,y), SAVELASTdo i = 1, nx = ...y = ...end do... = i... = x... = yこの例では、変数
x
、y
、i
がSTOREBACK
変数です。
REDUCTION
(varlist)
REDUCTION
(varlist) 修飾子は、変数リスト varlist 中のすべての変数がDOALL
ループにおいて縮約変数であることを指定します。縮約変数 (または配列) とは、その部分的な値を別々のプロセッサ上で個々に計算し、その部分的な値をもとにして最後の値を計算できる変数のことです。縮約変数のリストを指定すると、コンパイラが、
DOALL
ループが縮約ループであるかどうかを識別し、そのループの並列縮約コードを生成するのを助けます。
C$PAR DOALL REDUCTION(x)do i = 1, nx = x + a(i)end do上記の例では、変数
x
は (合計の) 縮約変数です。i
ループは (合計の) 縮約ループです。
SCHEDTYPE
(t)
SCHEDTYPE
(t) 修飾子は、特定のスケジューリング型を指定してDOALL
ループをスケジュールすることを指定します。
複数の修飾子
修飾子は複数回指定でき、この場合は効果が累積されます。修飾子が衝突する場合は、警告メッセージが出力され、最後に出現する修飾子が優先されます。
例 : 3 行の Sun 形式の指令です (
MAXCPUS
、SHARED、PRIVATE
修飾子の衝突に注意)。
C$PAR DOALL MAXCPUS(4) READONLY(S) PRIVATE(A,B,X) MAXCPUS(2)C$PAR DOALL SHARED(B,X,Y) PRIVATE(Y,Z)C$PAR DOALL READONLY(T)
C$PAR DOALL MAXCPUS(2), PRIVATE(A,Y,Z), SHARED(B,X), READONLY(S,T)
DOSERIAL
指令
DOSERIAL
指令は、指定したループの並列化を無効にします。この指令は、指令の直後にあるループ 1 つだけに適用されます。
do i = 1, nC$PAR DOSERIALdo j = 1, ndo k = 1, n...end doend doend doこの例では、
-parallel
を指定してコンパイルすると、j
ループは並列化されませんが、i
またはk
ループは並列化されます。
DOSERIAL*
指令
DOSERIAL*
指令は、ループの指定した入れ子の並列化を無効にします。この指令は、指令の直後にあるループの入れ子全体に適用されます。
do i = 1, nC$PAR DOSERIAL*do j = 1, ndo k = 1, n...end doend doend do上記のループで parallel を用いてコンパイルすると、
j
のループは並列化されず、i
またはk
ループが並列化されます。
DOSERIAL*
とDOALL
の相互作用
DOSERIAL*
とDOALL
の両方が同じループに指定されている場合、最後の指令が使用されます。例 :
DOSERIAL
とDOALL
を両方とも指定します。
C$PAR DOSERIAL*do i = 1, 1000C$PAR DOALLdo j = 1, 1000...end doend do上記の例では、
i
ループは並列化されず、j
ループは並列化されます。また、
DOSERIAL*
指令のスコープは、テキスト上でDOSERIAL*
指令の直後にあるループの入れ子を超えることはありません。DOSERIAL
* 指令は、DOSERIAL
* 指令がある関数またはサブルーチンに限定されます。例 :
DOSERIAL*
は、呼び出されたサブルーチンのループまで拡張されません。
program callercommon /block/ a(10,10)C$PAR DOSERIAL*do i = 1, 10call callee(i)end doendsubroutine callee(k)common /block/a(10,10)do j = 1, 10a(j,k) = j + kend doreturnend上記の例では、サブルーチン
callee
への呼び出しがインライン化されているかどうかにかかわらず、DOSERIAL*
はi
ループにしか適用されず、j
ループには適用されません。Sun 形式のデフォルトのスコープ規則
Sun 形式 (
C$PAR
) の明示的な指令では、コンパイラはデフォルトの規則を適用して、スカラーや配列が共有か非公開かを判別します。デフォルトの規則を変更するには、ループの中で参照されるスカラーや配列の属性を指定します。Cray 形式の!MIC$
指令では、ループ内に現れるすべての変数は、DOALL
指令を使用して、共有か非公開用かを明示的に宣言しなければなりません。
- スカラーはすべて非公開として扱われます。スレッドがループを実行するたびに局所的なスカラーのコピーが作成され、その局所的なコピーはスレッドでのみ使用されます。
- 配列参照はすべて共有参照として扱われます。あるスレッドが配列要素へ書き込んだ内容は、どのスレッドからも参照できます。ただし、共有変数へのアクセス時に、同期処理は行われません。
ループに反復間依存性が存在する場合、実行すると間違った結果になる可能性があります。ユーザーは、このような事態が発生しないように注意しなければなりません。コンパイラは、このような状況をコンパイル時に検出し、警告を発することもあります。しかし、コンパイラは、このようなループの並列化を無効にするわけではありません。
例: 問題が発生する可能性がある equivalence 文
equivalence (a(1),y)C$PAR DOALLdo i = 1,ny = ia(i) = yend doこの例では、スカラー変数
y
はa(1)
と等価であるため、デフォルトでこのスカラー変数y
を非公開変数として、またa(:)
を共有変数として扱ってしまいます。つまり、並列化されたI
ループを実行したときに、間違った結果を引き起こす可能性があります。この場合、診断は発行されません。この例を修正するには、
C$PAR
、DOALL
、PRIVATE
(y)
を使用します。Cray 形式の並列化指令
並列化指令には Sun 形式と Cray 形式の 2 つの形式があります。
f77
とf95
のデフォルトは Sun 形式 (-mp=sun
) です。Cray 形式の指令を使用する場合は、-mp=cray
を付けてコンパイルする必要があります。Sun 形式の指令を付けてコンパイルしたプログラム単位と Cray 形式の指令を付けてコンパイルしたプログラム単位を混在させると、異なる結果を生成する可能性があります。
Sun 形式の指令と Cray 形式の指令の主な違いは、Cray 形式では、ループ中のすべてのスカラーと配列に対して、
SHARED
かPRIVATE
のどちらかによる明示的なスコープの指定が必要であることです。
!MIC$ DOALL
!MIC$& SHARED(
v1,
v2, ... )
!MIC$& PRlIVATE(
u1,
u2, ... )
...任意の修飾子
Cray 形式の指令の構文
並列化指令は、1 つまたは複数の指令行から構成されます。指令行は、次の点を除いて、Sun 形式 (194ページ) と同じ構文で定義されます。
- 符号は
CMIC$
、*MIC$
、!MIC$
のいずれかでありますが、f95
の自由形式で認識されるのは!MIC$
だけです。- ループ内で参照されているすべての変数や配列は、
SHARED
修飾子またはPRIVATE
修飾子に記述します。
Cray 指令 Sun 形式との比較 DOALL修飾子セットとスケジューリングが異なります。 TASKCOMMONSun 形式と同じです。 DOSERIALSun 形式と同じです。 DOSERIAL*Sun 形式と同じです。
DOALL
修飾子Cray 形式の指令では、
PRIVATE
修飾子が必要です。DO
ループ内の各変数は、非公開または共有として修飾されなければならず、DO
ループの添字は常に非公開でなければなりません。次の表に、利用可能な Cray 形式の修飾子を要約します。
Cray 形式の指令では、
DOALL
指令は、1 つのスケジューリング修飾子を指定できます (たとえば、!MIC$& CHUNKSIZE(100)
)。表 10-8 に、Cray 形式のDOALL
指令を示します。
f77
でもf95
でも、デフォルトのスケジューリング型は Sun 形式のSTATIC
です(Cray 形式のDOALL
指令でスケジューリング型が指定されていない場合)。これと等価の Cray 形式のスケジューリング型はありません。環境変数
(「スタック、スタックサイズ、並列化」 の説明も参照してください)
PARALLEL
とOMP_NUM_THREADS
並列化されたプログラムをマルチスレッド環境で実行するには、実行前に、
PARALLEL
またはOMP_NUM_THREAD
S 環境変数を設定しなければなりません。これにより、実行時システムに、プログラムで作成可能なスレッドの最大数が知らされます。デフォルトは 1 です。一般に、PARALLEL
またはOMP_NUM_THREADS
環境変数には、ターゲットプラットフォームで使用可能なプロセッサ数を設定します。
SUNW_MP_THR_IDLE
プログラムの並列処理を実行する各スレッドのタスク終端ステータスを制御するには、
SUNW_MP_THR_IDLE
環境変数を使用します。この変数には、spin
、sleep
ns
、sleep
nms
のいずれかを設定できます。デフォルトは spin です。この場合、並列タスクの分担の処理が終わったとき、新しい並列タスクが届くまでスレッドはスピン待ちすることを意味します。そのほかの値を選択すると、スレッドは n 秒 (ns
) または n ミリ秒 (nms
) 間のスピン待ち後にスリープ状態になります。この待ち時間を超える前に新しいタスクが届くと、スレッドはスピン待機を中断し、新しいタスクを開始します。
%setenv SUNW_MP_THR_IDLE 50ms
%setenv PARALLEL 4
%myprog
この例では、プログラムで多くても 4 個のスレッドが作成されます。並列タスクが終了した後、スレッドは 50 ミリ秒間スピン待機します。その時間内にそのスレッドに新しいタスクが到着すると、スレッドはそのタスクを実行します。それ以外の場合、スレッドは新しいタスクが届くまでスリープ状態に入ります。
並列化されたプログラムをデバッグする
並列化されたプログラムをデバッグするには、新たな作業が必要になります。次に、その方法をいくつか示します。
dbx
を使用しないデバッグ並列化されたプログラムをデバッグするには、ちょっとした技術が必要です。
- 並列化をオフにする。
- 並列化オプションをオフにする。並列化を行わず、
-O3
か-O4
を付けてコンパイルし、プログラムが正しく動作するかを確認します。- スレッド数を 1 に設定し、並列化オプションをオンにしてコンパイルします。つまり、環境変数
PARALLEL
に1
を設定してプログラムを実行します。-reduction
をオフにする。- 個々のループの自動並列化オプションを選択しながらオフにするには、
DOSERIAL
指令を使用します。fsplit
またはf90split
を使用する。
- ユーザーのプログラムに多数のサブルーチンがある場合は、
fsplit
を使用してサブルーチンを別個のファイルに分割します。(Fortran 90 のソースコードではf90split
(1) を使用します。) そして、一部のサブルーチンに-parallel
を付け、一部には付けずにコンパイルしてみます。次に、f77
またはf90
を使用して.o
ファイルをリンクします。このリンクステップでも-parallel
を指定する必要があります。(『Fortran ユーザーズガイド』のコンパイルとリンクの整合性について説明している箇所を参照してください。)- バイナリを実行し、結果を検証します。(Fortran ユーザーズガイドのコンパイルとリンクの整合性について説明している箇所を参照してください。)
- この手順を繰り返して、問題を 1 つのサブルーチンにしぼり込みます。
-loopinfo
を使用する。- ダミーのサブルーチンを使用する。
- 何もしないダミーのサブルーチンや関数を作成します。並列化されているいくつかのループにこのサブルーチンへの呼び出しを挿入します。そして、コンパイルし直して、実行します。
-loopinfo
を使用して、どのループが並列化されているかを調べます。- 正しい結果が得られるようになるまで、この処理を続けます。
- 明示的な並列化を使用する。
- 並列化されている 2、3 のループに
C$PAR DOALL
指令を追加します。-explicitpar
を付けてコンパイルし、実行し、その結果を検証します。-loopinfo
を使用して、どのループが並列化されているかを調べます。この方法で、並列化されたループに入出力文も追加できます。- この手順を繰り返して、間違った結果の原因となるループを突き止めます。
注 --explicitpar
だけが必要な場合 (-autopar
は必要でない場合) は、-explicitpar
と-depend
を付けてコンパイルしないでください。この方法は、-parallel
を付けてコンパイルするのと同じことであり、この結果、-autopar
が含まれてしまいます。
- ループを逆方向に逐次実行する。
- ループインデックスを使用しない。
次のコードは:DO I=1,N...CALL SNUBBER(I)...ENDDO次のように書き換える:DO I1=1,NI=I1
...CALL SNUBBER(I)...ENDDO
dbx
による並列コードのデバッグ並列化されたループに
dbx
を使用するためには、一時的にプログラムを次のように書き直します。
- ループ本体を入れるファイルと、サブルーチンを入れるファイルを別々に分けます。
- オリジナルのルーチンでは、ループ本体を新しいサブルーチンの呼び出しで置き換えます。
- 新しいサブルーチンは、
-g
を付けて、並列化オプションを付けずにコンパイルします。- 変更されたオリジナルのルーチンは、並列化オプションを付けて、
-g
を付けずにコンパイルします。例 : 並列化されたプログラムで
dbx
が使用できるように、ループを手作業で変形します。
サン・マイクロシステムズ株式会社 Copyright information. All rights reserved. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |