アプリケーションの並列化 (またはマルチスレッド化) とは、マルチプロセッサシステム上で実行できるよう、またはマルチスレッド環境、コンパイルされたプログラムを分散することです。 並列化によって、1 つのタスク (DO ループなど) を複数のプロセッサ (またはスレッド) を使って実行できるので、実行速度が上がる可能性があります。
UltraTM 60、Sun EnterpriseTM Server 6500、または Sun Enterprise Server 10000 のようなマルチプロセッサシステム上でアプリケーションプログラムを効率的に実行できるようにするためには、そのアプリケーションプログラムをマルチスレッド化する必要があります。つまり、並列実行できるタスクを識別し、複数のプロセッサまたはスレッドを横にしてその計算を分配するようにプログラムを変更する必要があります。
アプリケーションのマルチスレッド化は、libthread プリミティブを適切に呼び出すことによって、手作業で行うことができます。しかし、膨大な量の解析とプログラムの変更が必要となります。詳細は、Solaris の『マルチスレッドのプログラミング』を参照してください。
Sun コンパイラは、マルチプロセッサシステム上で動作できるようにマルチスレッド化されたオブジェクトコードを自動的に生成できます。Fortran コンパイラは、並列性をサポートする主要な言語要素としての DO ループに焦点をあわせます。並列化は、Fortran ソースプログラムに一切手を加えることなく、ループの計算作業を複数のプロセッサに分配します。
どのループを並列化するか、またそのループをどのように分配するかは、完全にコンパイラに任せることも (-autopar)、ソースコード指令を使用してプログラマが明示的に決定することも (-explicitpar)、その両方を組み合わせることも (-parallel) できます。
独自の (明示的な) スレッド管理を行うプログラムをコンパイルするときは、コンパイラのどのような並列化オプションも付けてはいけません。明示的なマルチスレッド化 (libthread プリミティブへの呼び出し) は、並列化オプションを付けてコンパイルしたルーチンと組み合わせることはできません。
プログラム中のすべてのループが有効に並列化されるわけではありません。計算作業量の少ないループを並列化すると、(並列タスクの起動と同期に費やされるオーバーヘッドと比べると) 実際には実行が遅くなることもあります。また、安全に並列化できないループもあります。 このようなループは、文間あるいは反復間の依存関係のため、並列化すると異なる結果を生成します。
明示的な DO ループとともに暗示的なループ (IF ループと Fortran 95 配列構文など) が、Fortran コンパイラでの自動並列化の対象となります。
f95 は、安全にそして有効に並列化できる可能性のあるループを自動的に検出できます。しかし、ほとんどの場合、隠れた副作用のおそれがあるので、この解析はどうしても控え目になります。どのループが並列化され、どのループが並列化されていないかは、-loopinfo オプションで表示できます。ループの前にソースコード指令を挿入することによって、特定のループを並列化するかどうかを明示的に制御できます。しかし、このように明示的に並列化を指定したループによって結果が間違ったとしても、それはユーザーの責任になります。
Fortran 95 コンパイラは、OpenMP 2.0 Fortran API 指令を実装することによって明示的に並列化を行います。古いプログラムに対応するために、f95 は古い Sun 形式および Cray 形式の指令も受け入れますが、それらの指令の使用は、現在では推奨されていません。OpenMP は、Fortran 95、C、C++ での明示的な並列化の非公式の標準となっています。 古い指令形式には OpenMP をお勧めします。
Open MP については、『OpenMP API ユーザーズガイド』か、OpenMP の Web サイト (http://www.openmp.org) を参照してください。
4 つのプロセッサ上で動作するようにプログラムを並列化した場合、そのプログラムは、1 つのプロセッサ上で動作させるときの約 1/4 の時間で処理できる (4 倍の速度向上になる) と期待できるでしょうか。
おそらく、答えは「ノー」です。プログラムの全体的な速度向上は、並列実行しているコード中で消費される実行時間の割り合いによって厳密に制限されると証明できます (アムダールの法則)。適用されるプロセッサがいくつになろうとも、これは常に真です。事実、並列実行した実行プログラムの合計時間のパーセンテージを p とすると、理論的な速度向上の制限は 100/(100–p) となります。したがって、プログラムの 60% だけが並列実行した場合、プロセッサの数にかかわらず、速度向上は最大 2.5 倍です。そして、プロセッサが 4 つの場合、このプログラムの理論的な速度向上は (最大限の効率が発揮されたと仮定しても) 1.8 倍です。4 倍にはなりません。オーバーヘッドがあれば、実際の速度向上はさらに小さくなります。
最適化のことを考えると、ループの選択は重要です。プログラムの合計実行時間のほんの一部としか関わらないループを並列化しても、最小の効果しか得られません。効果を得るためには、実行時間の大部分を消費するループを並列化しなければなりません。したがって、どのループが重要であるかを決定し、そこから始めるのが第一歩です。
問題のサイズも、並列実行するプログラムの割合を決定するのに重要な役割を果たし、その結果、速度向上にもつながります。問題のサイズを増やすと、ループの中で行われる作業量も増えます。3 重に入れ子にされたループは、作業量が 3 乗になる可能性があります。入れ子の外側のループを並列化する場合、問題のサイズを少し増やすと、並列化していないときのパフォーマンスと比べて、パフォーマンスが大幅に向上します。
次に、アプリケーションの並列化に必要な手順について、極めて一般的な概要を示します。
最適化。適切なコンパイラオプションのセットを使用して、1 つのプロセッサ上で最高のパフォーマンスを得ます。
プロファイル。典型的なテストデータを使用して、プログラムのパフォーマンスプロファイルを決定します。もっとも重要なループを見つけます。
ベンチマーク。逐次処理でのテストの結果が正確かどうかを決定します。これらの結果とパフォーマンスプロファイルをベンチマークとして使用します。
並列化。オプションと指令の組み合わせを使用して、並列化した実行可能ファイルをコンパイルし、構築します。
検証。並列化したプログラムを 1 つのプロセッサや 1 つのスレッド上で実行し、結果を検査して、その中の不安定さやプログラミングエラーを見つけます。$PARALLEL または$OMP_NUM_THREADS に 1 を設定します。「10.1.5 スレッドの数」を参照してください。
テスト。複数のプロセッサ上でさまざまな実行を試し、結果を検査します。
ベンチマーク。専用のシステムで、プロセッサの数を変えながらパフォーマンスを測定します。問題のサイズを変化させて、性能の変化を測定します (スケーラビリティー)。
ステップ 4 〜 7 を繰り返します。パフォーマンスに基づいて、並列化スキームを改良します。
すべてのループが並列化できるわけではありません。複数のプロセッサ上でループを並列実行すると、実行している反復の順序が変わる可能性があります。さらに、ループを並列実行する複数のプロセッサがお互いに干渉する可能性もあります。 このような状況が発生するのは、ループ中にデータ依存性がある場合です。
データ依存性の問題が発生する場合は、再帰、縮約、間接アドレス指定、データに依存するループが繰り返されています。
ループを書き直して、並列化することで、データへの依存をなくすことができます。しかし、拡張再構成が必要な場合があります。
次は、いくつかの一般的な規則です。
すべての繰り返しが個々のメモリー位置に書き込む場合のみ、ループはデータから独立しています。
いずれの繰り返しも同じ位置に書き込まないかぎり、繰り返しはその位置から読み取る場合があります。
これらは並列化の一般的な条件です。ループを並列化するかどうか決める際に、コンパイラの自動並列化解析により、追加の条件が検討されます。しかし、抑制により誤った結果を出すループも含め、ループを明示的に並列化する指令を使用することができます。
ループのある反復で設定され、後続の反復で使用される変数は、反復間依存性、つまり再帰の原因となります。ループ中で再帰を行う場合は、反復が適切な順序で実行される必要があります。たとえば、次のようにします。
DO I=2,N A(I) = A(I-1)*B(I)+C(I) END DO |
たとえば、前述のコードでは、以前の反復中で A(I) 用に計算された値が、現在の反復中で (A(I-1) として) 使用されなければいけません。各反復を並列実行して、1 つのプロセッサで実行したときと同じ結果を生成するためには、反復 I は、反復 I+1 が実行できる前に完了している必要があります。
縮約操作は、配列の要素を 1 つの値に縮約します。たとえば、配列の要素の合計を 1 つの変数にまとめる場合、その変数は反復ごとに更新されます。
DO K = 1,N SUM = SUM + A(I)*B(I) END DO |
このループを並列実行する各プロセッサが反復のサブセットを取る場合、SUM の値を上書きしようとして、各プロセッサはお互いに干渉します。うまく処理するためには、各プロセッサが 1 度に 1 回ずつ合計を実行する必要があります。しかし、順序は問題になりません。
ある共通の縮約操作は、コンパイラによって、特別なケースであると認識され、処理されます。
ループ依存性は、値が未知である添字によってループの中の添字付けられた配列への格納から発生する可能性があります。たとえば、添字付の配列中に繰り返される値がある場合、間接アドレス指定は順序に依存することがあります。
DO L = 1,NW A(ID(L)) = A(L) + B(L) END DO |
前述の例中、ID 中で繰り返される値は、 A の要素を上書きする原因となります。逐次処理の場合、最後の格納が最終値です。並列処理の場合、順序は決定されていません。使用される A(L) の値 (古い値か更新された値) は、順序に依存します。
Sun Studio コンパイラは、OpenMP 並列化モデルを基本並列化モデルとしてネイティブにサポートするようになりました。OpenMP 並列化についての詳細は、『OpenMP API ユーザーズガイド』を参照してください。Sun および Cray 形式の並列化とは、古いアプリケーションを意味し、現在の Sun Studio コンパイラではサポートされていません。
表 10–1 Fortran 95 の並列化オプション
オプション |
フラグ |
---|---|
自動 (のみ) |
-autopar |
自動、縮約 |
-autopar -reduction |
並列化されるループを表示 |
-loopinfo |
明示に関連する警告を表示 |
-vpara |
局所変数をスタックに割り当て |
-stackvar |
OpenMP 並列化用にコンパイル |
-xopenmp |
オプションについての注意
これらのオプションの多くには、-autopar や -xautopar などの同等の同義語があります。どちらも使用することができます。
コンパイラの prof/gprof プロファイルオプションの -p、-xpg、および -pg は、ほかの並列化オプションのいずれとも組み合わせないでください。これらプロファイルオプションの実行時サポートは、スレッドに対して安全ではありません。実行時に、不正な結果やセグメント例外が発生する可能性があります。
-reduction を指定するときは -autopar も必要です。
-autopar には -depend とループ構造の最適化が含まれます。
-noautopar、-noreduction は打消しオプションです。
並列化オプションはどのような順序で指定してもかまいません。ただし、必ずすべてを小文字にしなければいけません。
明示的に並列化されたループでは、縮約操作は解析されません。
-openmp は -stackvar を自動的に起動します。
オプション -loopinfo と -vpara は、ほかのいずれかの並列化オプションとともに使用しなければいけません。
PARALLEL (または OMP_NUM_THREADS) 環境変数は、プログラムで使用可能なスレッドの最大数を制御します。環境変数を設定することにより、実行時システムに、プログラムで使用可能なスレッドの最大数が知らされます。デフォルトは 1 です。 一般的に、PARALLEL 変数または OMP_NUM_THREADS 変数には、ターゲットプラットフォームで使用可能なプロセッサ数を設定します。
次の例で、その設定方法を示します。
demo% setenv OMP_NUM_THREADS 4 C shell |
または
demo$ OMP_NUM_THREADS=4 Bourne/Korn shell demo$ export OMP_NUM_THREADS |
前述の例では、PARALLEL を 4 に設定することで、プログラムの実行は最大 4 つのスレッドを使用できます。ターゲットマシンが 4 つのプロセッサを利用できる場合、各スレッドはプロセッサ 1 つずつにマップされます。利用可能なプロセッサが 4 つより少ない場合、スレッドのいくつかはほかのスレッドと同じプロセッサ上で実行されるので、パフォーマンスは下がります。
SunOSTM コマンド psrinfo(1M) は、システムで利用可能なプロセッサのリストを表示します。
demo% psrinfo 0 オンライン 03/18/2007 15:51:03 から 1 オンライン 03/18/2007 15:51:03 から 2 オンライン 03/18/2007 15:51:03 から 3 オンライン 03/18/2007 15:51:03 から |
プログラムの実行は、プログラムを最初に実行したスレッドのためにメインメモリーのスタックを保持し、各ヘルパースレッドのために個々のスタックを保持します。スタックとは、副プログラムの呼び出し時に引数と AUTOMATIC 変数を保持するために使用される一時的なメモリーアドレス空間です。
メインスタックのデフォルトのサイズは、約 8M バイトです。Fortran コンパイラは、通常、局所変数と配列を (スタックにではなく) STATIC として割り当てます。しかし、-stackvar オプションを使用すると、すべての局所変数と配列をスタックに割り当てます (あたかもそれが AUTOMATIC 変数であるかのように)。-stackvar は並列化とともに使用することを推奨します。なぜなら、ループ中の CALL を並列化するオプティマイザの能力を向上させるからです。 -stackvar は、副プログラム呼び出しを持つ明示的に並列化されたループには必須です。-stackvar については、『Fortran ユーザーズガイド』を参照してください。
C シェル (csh) を使用し limit コマンドにより現在のメインスタックのサイズを表示し、設定します。
demo% limit C シェルの例 cputime 制限無し filesize 制限無し datasize 2097148 kbytes stacksize 8192 kbytes <- 現在のメインスタックのサイズ coredumpsize 0 kbytes descriptors 64 memorysize 制限無し demo% limit stacksize 65536 <- メインスタックを 64M バイトに設定 demo% limit stacksize stacksize 65536 kbytes |
Bourne シェルまたは Korn シェルの場合、対応するコマンドは ulimit です。
demo$ ulimit -a Korn シェルの例 time(seconds) 制限無し file(blocks) 制限無し data(kbytes) 2097148 stack(kbytes) 8192 coredump(blocks) 0 nofiles(descriptors) 64 vmemory(kbytes) 制限無し demo$ ulimit -s 65536 demo$ ulimit -s 65536 |
マルチスレッド化されたプログラムの各スレッドは、独自のスレッドスタックを持っています。このスタックは、初期スレッドのスタックと似ています。 しかし、スレッド固有のものです。スレッドの PRIVATE 配列と変数 (スレッドに局所的な) は、スレッドスタックに割り当てられます。 64 ビット SPARC および 64 ビット x86 プラットフォームでのデフォルトのサイズは 8M バイトです。そのほかのプラットフォームでは 4M バイトです。 このサイズは、STACKSIZE 環境変数で設定されます。
demo% setenv STACKSIZE 8192 <- スレッドスタックサイズを 8M バイトに設定 C シェル または demo$ STACKSIZE=8192 Bourne/Korn Shell demo$ export STACKSIZE |
いくつかの並列化された Fortran コードに対しては、スレッドスタックのサイズをデフォルトより大きく設定することが必要になります。しかし、どれくらいの大きさに設定すればいいのかを知る方法はなく、試行錯誤してみるしかありません。 特に、専用配列または局所配列が関連する場合はわかりません。スタックのサイズが小さすぎてスレッドが実行できない場合、プログラムはセグメンテーションフォルトで異常終了します。