Sun Studio 12: Fortran プログラミングガイド

第 10 章 並列化

この章では、マルチプロセッサの並列化の概要を示し、Solaris SPARC および x86 マルチプロセッサプラットフォーム上の Fortran 95 コンパイラの機能について説明します。

Rajat Garg および Ilya Sharapov 著、Sun Microsystems Blueprints『Techniques for Optimizing Applications:High Performance Computing』も参照してください (http://www.sun.com/blueprints/pubs.html)。

10.1 基本概念

アプリケーションの並列化 (またはマルチスレッド化) とは、マルチプロセッサシステム上で実行できるよう、またはマルチスレッド環境、コンパイルされたプログラムを分散することです。 並列化によって、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) を参照してください。

10.1.1 速度向上 — 何を期待するか

4 つのプロセッサ上で動作するようにプログラムを並列化した場合、そのプログラムは、1 つのプロセッサ上で動作させるときの約 1/4 の時間で処理できる (4 倍の速度向上になる) と期待できるでしょうか。

おそらく、答えは「ノー」です。プログラムの全体的な速度向上は、並列実行しているコード中で消費される実行時間の割り合いによって厳密に制限されると証明できます (アムダールの法則)。適用されるプロセッサがいくつになろうとも、これは常に真です。事実、並列実行した実行プログラムの合計時間のパーセンテージを p とすると、理論的な速度向上の制限は 100/(100–p) となります。したがって、プログラムの 60% だけが並列実行した場合、プロセッサの数にかかわらず、速度向上は最大 2.5 倍です。そして、プロセッサが 4 つの場合、このプログラムの理論的な速度向上は (最大限の効率が発揮されたと仮定しても) 1.8 倍です。4 倍にはなりません。オーバーヘッドがあれば、実際の速度向上はさらに小さくなります。

最適化のことを考えると、ループの選択は重要です。プログラムの合計実行時間のほんの一部としか関わらないループを並列化しても、最小の効果しか得られません。効果を得るためには、実行時間の大部分を消費するループを並列化しなければなりません。したがって、どのループが重要であるかを決定し、そこから始めるのが第一歩です。

問題のサイズも、並列実行するプログラムの割合を決定するのに重要な役割を果たし、その結果、速度向上にもつながります。問題のサイズを増やすと、ループの中で行われる作業量も増えます。3 重に入れ子にされたループは、作業量が 3 乗になる可能性があります。入れ子の外側のループを並列化する場合、問題のサイズを少し増やすと、並列化していないときのパフォーマンスと比べて、パフォーマンスが大幅に向上します。

10.1.2 プログラムの並列化のための手順

次に、アプリケーションの並列化に必要な手順について、極めて一般的な概要を示します。

  1. 最適化。適切なコンパイラオプションのセットを使用して、1 つのプロセッサ上で最高のパフォーマンスを得ます。

  2. プロファイル。典型的なテストデータを使用して、プログラムのパフォーマンスプロファイルを決定します。もっとも重要なループを見つけます。

  3. ベンチマーク。逐次処理でのテストの結果が正確かどうかを決定します。これらの結果とパフォーマンスプロファイルをベンチマークとして使用します。

  4. 並列化。オプションと指令の組み合わせを使用して、並列化した実行可能ファイルをコンパイルし、構築します。

  5. 検証。並列化したプログラムを 1 つのプロセッサや 1 つのスレッド上で実行し、結果を検査して、その中の不安定さやプログラミングエラーを見つけます。$PARALLEL または$OMP_NUM_THREADS に 1 を設定します。「10.1.5 スレッドの数」を参照してください。

  6. テスト。複数のプロセッサ上でさまざまな実行を試し、結果を検査します。

  7. ベンチマーク。専用のシステムで、プロセッサの数を変えながらパフォーマンスを測定します。問題のサイズを変化させて、性能の変化を測定します (スケーラビリティー)。

  8. ステップ 4 〜 7 を繰り返します。パフォーマンスに基づいて、並列化スキームを改良します。

10.1.3 データ依存性の問題

すべてのループが並列化できるわけではありません。複数のプロセッサ上でループを並列実行すると、実行している反復の順序が変わる可能性があります。さらに、ループを並列実行する複数のプロセッサがお互いに干渉する可能性もあります。 このような状況が発生するのは、ループ中にデータ依存性がある場合です。

データ依存性の問題が発生する場合は、再帰、縮約、間接アドレス指定、データに依存するループが繰り返されています。

10.1.3.1 データに依存したループ

ループを書き直して、並列化することで、データへの依存をなくすことができます。しかし、拡張再構成が必要な場合があります。

次は、いくつかの一般的な規則です。

これらは並列化の一般的な条件です。ループを並列化するかどうか決める際に、コンパイラの自動並列化解析により、追加の条件が検討されます。しかし、抑制により誤った結果を出すループも含め、ループを明示的に並列化する指令を使用することができます。

10.1.3.2 再帰

ループのある反復で設定され、後続の反復で使用される変数は、反復間依存性、つまり再帰の原因となります。ループ中で再帰を行う場合は、反復が適切な順序で実行される必要があります。たとえば、次のようにします。


   DO I=2,N
      A(I) = A(I-1)*B(I)+C(I)
   END DO

たとえば、前述のコードでは、以前の反復中で A(I) 用に計算された値が、現在の反復中で (A(I-1) として) 使用されなければいけません。各反復を並列実行して、1 つのプロセッサで実行したときと同じ結果を生成するためには、反復 I は、反復 I+1 が実行できる前に完了している必要があります。

10.1.3.3 縮約

縮約操作は、配列の要素を 1 つの値に縮約します。たとえば、配列の要素の合計を 1 つの変数にまとめる場合、その変数は反復ごとに更新されます。


   DO K = 1,N
     SUM = SUM + A(I)*B(I)
   END DO

このループを並列実行する各プロセッサが反復のサブセットを取る場合、SUM の値を上書きしようとして、各プロセッサはお互いに干渉します。うまく処理するためには、各プロセッサが 1 度に 1 回ずつ合計を実行する必要があります。しかし、順序は問題になりません。

ある共通の縮約操作は、コンパイラによって、特別なケースであると認識され、処理されます。

10.1.3.4 間接アドレス指定

ループ依存性は、値が未知である添字によってループの中の添字付けられた配列への格納から発生する可能性があります。たとえば、添字付の配列中に繰り返される値がある場合、間接アドレス指定は順序に依存することがあります。


   DO L = 1,NW
     A(ID(L)) = A(L) + B(L)
   END DO

前述の例中、ID 中で繰り返される値は、 A の要素を上書きする原因となります。逐次処理の場合、最後の格納が最終値です。並列処理の場合、順序は決定されていません。使用される A(L) の値 (古い値か更新された値) は、順序に依存します。

10.1.4 並列オプションと指令についての要約

Sun Studio コンパイラは、OpenMP 並列化モデルを基本並列化モデルとしてネイティブにサポートするようになりました。OpenMP 並列化についての詳細は、『OpenMP API ユーザーズガイド』を参照してください。Sun および Cray 形式の並列化とは、古いアプリケーションを意味し、現在の Sun Studio コンパイラではサポートされていません。

表 10–1 Fortran 95 の並列化オプション

オプション 

フラグ 

自動 (のみ)

-autopar

自動、縮約 

-autopar -reduction

並列化されるループを表示 

-loopinfo

明示に関連する警告を表示 

-vpara

局所変数をスタックに割り当て 

-stackvar

OpenMP 並列化用にコンパイル 

-xopenmp

オプションについての注意

10.1.5 スレッドの数

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 から

10.1.6 スタック、スタックサイズ、並列化

プログラムの実行は、プログラムを最初に実行したスレッドのためにメインメモリーのスタックを保持し、各ヘルパースレッドのために個々のスタックを保持します。スタックとは、副プログラムの呼び出し時に引数と 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 コードに対しては、スレッドスタックのサイズをデフォルトより大きく設定することが必要になります。しかし、どれくらいの大きさに設定すればいいのかを知る方法はなく、試行錯誤してみるしかありません。 特に、専用配列または局所配列が関連する場合はわかりません。スタックのサイズが小さすぎてスレッドが実行できない場合、プログラムはセグメンテーションフォルトで異常終了します。

10.2 自動並列化

-autopar オプションを使用すると f95 コンパイラは、効率的に並列化できる DO ループを自動的に見つけます。このようなループは変形され、利用可能なプロセッサに対してその反復が均等に分配されます。コンパイラは、このために必要なスレッド呼び出しを生成します。

10.2.1 ループの並列化

コンパイラによる依存性の解析は、DO ループを並列化可能なタスクに変形します。コンパイラは、ループの構造を変形して、逐次実行する、並列化できないセクションを切り離します。次に、利用可能なプロセッサに対して作業を均等に分配します。各プロセッサが反復の異なったブロックを実行します。

たとえば、4 つの CPU と 1,000 回の反復を持つ並列化ループの例で、各スレッドは 250 回の反復をまとめて実行します。

プロセッサ 1 が実行する反復 

から 

250 

プロセッサ 2 が実行する反復 

251 

から 

500 

プロセッサ 3 が実行する反復 

501 

から 

750 

プロセッサ 4 が実行する反復 

751 

から 

1000 

並列化できるのは、計算の実行順序に依存しないループだけです。コンパイラによる依存性の解析は、本質的にデータ依存性を持つループを拒否します。ループ中のデータフローを完全に決定できない場合、コンパイラは保守的に動作し、並列化を行いません。また、パフォーマンスの向上よりもオーバーヘッドが勝る場合、ループを並列化しないことを選択する可能性もあります。

コンパイラは常に、静的ループスケジューリング (つまり、ループ中の作業を単純に均等な反復ブロックに分割する方法) を使用して、ループを並列化することを選択することに注意してください。明示的な並列化指令を使用すれば、ほかの分配スキームも指定できます。 この指令については、この章の後半で説明します。

10.2.2 配列、スカラー、純スカラー

自動並列化という観点から、2、3 の定義が必要です。

例: 配列とスカラー


      dimension a(10)
      real m(100,10), s, u, x, z
      equivalence ( u, z )
      pointer ( px, x )
      s = 0.0
      ...

ma は両方とも配列変数です。s は純スカラーです。変数 uxzpx はスカラー変数ですが、純スカラーではありません。

10.2.3 自動並列化の基準

反復間データ依存性をもたない DO ループは、-autopar によって自動的に並列化されます。自動並列化のための一般的な基準は次のとおりです。

10.2.3.1 見かけの依存性

コンパイラは、コンパイルされたコードを変形するときに、ループ中のデータ依存の原因になりそうな (見かけの) 参照を自動的に取り除きます。このような多数の変換の 1 つは、一部の配列の専用バージョンを使用します。コンパイラがこの処理を行うことができるのは、一般的には、そのような配列が本来のループで一時領域としてのみ使用されていることが判断できる場合です。

例: -autopar を使用しています。専用配列によって依存が取り除かれます。


      parameter (n=1000)
      real a(n), b(n), c(n,n)
      do i = 1, 1000             <--並列化される
        do k = 1, n
          a(k) = b(k) + 2.0
        end do
        do j = 1, n-1
          c(i,j) = a(j+1) + 2.3
        end do
      end do
      end

前述の例では、外側のループが並列化され、別々のプロセッサ上で実行されます。配列 a を参照する内側のループはデータ依存性の原因になるように見えますが、コンパイラはその配列の一時的な専用コピーを作成して、外側のループの反復を依存しないようにしています。

10.2.3.2 自動並列化の抑制要因

自動並列化では、次のいずれかが発生すると、コンパイラはループを並列化しません。

10.2.3.3 入れ子にされたループ

マルチプロセッサシステムでは、もっとも内側のループではなく、ループの入れ子のもっとも外側のループを並列化するのがもっとも効果的です。並列処理は一般にループのオーバーヘッドがかなり大きいため、もっとも外側のループを並列化することでループのオーバーヘッドが最小になり、各プロセッサの処理量が最大になります。自動並列化では、コンパイラは入れ子のもっとも外側のループからループの解析を始め、並列化可能なループが見つかるまで、内側に進んでいきます。入れ子の中でループが 1 つでも並列化されたら、並列ループの中に含まれるループは無視されます。

10.2.4 縮約操作を使用した自動並列化

配列をスカラーに変形する計算のことを「縮約操作」と呼びます。典型的な縮約操作は、ベクトルの要素の合計や積です。 縮約操作は、ループ内の計算が反復にまたがって累積的に変数を変更しないという基準には反するものです。

例: ベクトルの要素の合計を縮訳する


      s = 0.0
      do i = 1, 1000
        s = s + v(i)
      end do
      t(k) = s

しかし、一部の操作では、並列化を妨げるのが縮約だけの場合は、この基準にかかわらず並列化できます。共通の縮約操作が頻繁に発生するので、コンパイラはこれらの操作を特別なケースであると認識し、並列化します。

-reduction コンパイラオプションが -autopar-parallel とともに指定されていなければ、縮約操作の認識は、自動並列化解析の中には含まれません。

並列化可能なループが表 10–2 にリストされた縮約操作のいずれか 1 つを持つ場合、-reduction が指定されていれば、コンパイラはそのループを並列化します。

10.2.4.1 認識される縮約操作

次の表に、f77 および f95 が認識する縮約操作をリストします。

表 10–2 認識される縮約操作

数学的な操作 

Fortran 文のテンプレート 

合計 

s = s + v(i)

積 

s = s * v(i)

ドット積 

s = s + v(i) * u(i)

最小 

s = amin( s, v(i))

最大 

s = amax( s, v(i))

OR

do i = 1, n

b = b .or. v(i)

end do

AND

b = .true.

do i = 1, n

b = b .and. v(i)

end do

ゼロでない要素の計数 

k = 0

do i = 1, n

if(v(i).ne.0) k = k + 1

end do

MIN 関数と MAX 関数はすべての形式で認識されます。

10.2.4.2 数値的な正確性と縮約操作

次の条件のため、浮動小数点の合計や積の縮約操作が不正確になることがあります。

状況によって、エラーが受け付けられない場合があります。

例: 丸めの例です。-1 と +1 の間の 100,000 個の乱数を合計します。


demo% cat t4.f
      parameter ( n = 100000 )
      double precision d_lcrans, lb / -1.0 /, s, ub / +1.0 /, v(n)
      s = d_lcrans ( v, n, lb, ub ) ! n 個の -1 と +1 の間の乱数を求める。
      s = 0.0
      do i = 1, n
        s = s + v(i)
      end do
      write(*, '(" s = ", e21.15)') s
      end
demo% f95 -O4 -autopar -reduction t4.f

結果は、プロセッサの数によって異なります。次の表に、-1 と +1 の間の 100,000 個の乱数の合計を示します。

プロセッサの数 

出力 

s = 0.568582080884714E+02

s = 0.568582080884722E+02

s = 0.568582080884721E+02

s = 0.568582080884724E+02

この状況では、丸めの誤差はおよそ 10-14 なので、この乱数のデータは容認できます。詳細は、『数値計算ガイド』を参照してください。

10.3 明示的な並列化

この節では、どのループを並列化するか、どの方針を使用するかを明示的に指示するための、f95 によって認識されるソースコード指令について説明します。

Fortran 95 コンパイラは、OpenMP Fortran API を基本並列化モデルとして完全にサポートするようになりました。詳細は、『OpenMP API ユーザーズガイド』を参照してください。

従来の Sun 形式と Cray 形式の並列化指令は、SPARC プラットフォームの Sun Studio コンパイラでサポートされなくなりました。また、x86 プラットフォームのコンパイラでは受け入れられません。

プログラムを明示的に並列化するためには、アプリケーションコードの事前解析と深い理解、そして、共有メモリー並列化の概念が必要です。

DO ループに並列化のためのマークを付けるには、ループの直前に指令を置きます。OpenMP Fortran 95 指令が認識されて DO ループが並列化されるようにするには、-openmp を使用してコンパイルします。並列化指令は、その指令後の DO ループを並列化する (または並列化しない) ようにコンパイラに伝えるコメント行です。指令は、プラグマともいいます。

どのループに並列化のマークを付けるかを選択するときは注意してください。並列を実行するときに間違った結果を計算してしまうデータ依存性がループにある場合でも、コンパイラは、DOALL 指令でマークを付けられたすべてのループに対して、スレッド化された並列コードを生成します。

libthread プリミティブを使用して独自のマルチスレッド化コーディングを行う場合は、コンパイラのいかなる並列化オプションも付けてはいけません。コンパイラは、スレッドライブラリへのユーザーの呼び出しを使用してすでに並列化されているコードを並列化することはできません。

10.3.1 並列可能なループ

次のような場合、ループは明示的な並列化に適しています。

10.3.1.1 スコープ規則: 非公開と共有

非公開変数または専用配列は、ループの 1 回の反復だけで使用されます。ある反復で非公開変数または非公開配列に代入された値は、そのループの別の反復には伝達されません。

共有変数または共有配列は、ほかのすべての反復で共有されます。ある反復で共有変数または共有配列に代入された値は、そのループの別の反復からも参照されます。

明示的に並列化されたループで共有の値を参照する場合、共有によって正確性の問題が発生しないように注意してください。共有変数が更新またはアクセスされたとき、コンパイラは同期処理を行いません。

あるループの中で変数が非公開であると指定した場合、さらに、その変数の唯一の初期化がほかのループの中にある場合、その変数の値はループの中で未定義のままとなる可能性があります。

10.3.1.2 ループでの副プログラム呼び出し

ループで (または呼び出し元ルーチン内から呼び出された副プログラムで) 副プログラムを呼び出すと、データ依存性が生じる可能性があり、これは呼び出しのチェーンをたどってデータや制御フローを深く分析しなければ気づかないでしょう。作業量の多い一番外側のループを並列化すればよいのですが、これらは副プログラムをいくつも呼び出してループがとても深くなっている傾向があります。

このような手続き間の分析は難しく、またコンパイル時間がかなり長くなってしまうので、自動並列化モードでは行われません。明示的な並列化では、コンパイラは、PARALLEL DO または DOALL 指令によりマークしたループ内にサブプログラムへの呼び出しが含まれていても、そのループの並列化コードを生成します。この場合も、ループ内に、また呼び出し先サブプログラムを含めてループ内のすべてにおいてデータ依存が存在しないようにすることはプログラマの仕事です。

さまざまなスレッドから 1 つのルーチンを何度も起動すると、局所静的変数への参照でお互いに干渉し合うような問題が発生することがあります。ルーチン内のすべての局所変数を静的変数ではなく自動変数にすることで、この問題は防ぐことができます。このようにしてサブプログラムを起動すると、そのたびに局所変数が固有の領域に保存され、それらがスタック上で保守されるので、何度起動してもお互いに干渉することはなくなります。

局所サブプログラム変数は、自動変数にすることが可能で、AUTOMATIC 文で指定するか、または -stackvar オプションを指定してサブプログラムをコンパイルすることでスタック上に常駐させることができます。ただし、DATA 文で初期化された局所変数については、実際の割り当てで初期化されるように書きかえる必要があります。


注 –

局所変数をスタックに割り当てると、スタックがオーバーフローしてしまう可能性があります。スタックのサイズを大きくする方法については、「10.1.6 スタック、スタックサイズ、並列化」を参照してください。


10.3.1.3 明示的並列化の抑制

一般に、ユーザーがコンパイラにループを並列化するように明示的に指示している場合、コンパイラはそのようにします。ただし、例外もあり、ループによってはコンパイラが並列化を行わないものがあります。

次に、DO ループの明示的な並列化を妨げる抑制の中で、検出可能なものを示します。

-vpara および -loopinfo を指定してコンパイルすると、コンパイラが明示的にループを並列化している最中に問題を検出すると診断メッセージが発せられます。

次に、一般にコンパイラにより検出される並列化の問題を示します。

表 10–3 明示的な並列化時の問題

問題 

並列化されます 

警告メッセージ 

ループは、並列化されている別のループ内に入れ子にされています。 

いいえ 

いいえ 

ループは、並列化されたループの本文内で呼び出されているサブルーチン内にあります。 

いいえ 

いいえ 

フロー制御文で、ループから外部へのジャンプが許可されています。 

いいえ 

はい 

ループの添字変数が、悪影響を受けています。 

はい 

いいえ 

ループ内の変数に、ループ繰越の依存があります。 

はい 

はい 

ループ内の入出力文 — 通常、出力順序は予想できないので賢明な処理ではありません。

はい 

いいえ 

例: 入れ子にされたループ


      ...
!$OMP PARALLEL DO
      do 900 i = 1, 1000      !  並列化されます (外側のループ)
        do 200 j = 1, 1000    !  並列化されません。警告も発しません
            ...
200   continue
900      continue
      ...

例: サブルーチン内で並列化されたループ


 program main
      ...
!$OMP PARALLEL DO
      do 100 i = 1, 200      <- 並列化されます
        ...
        call calc (a, x)
        ...
100      continue
      ...
subroutine calc ( b, y )
      ...
!$OMP PARALLEL DO
      do 1 m = 1, 1000       <- 並列化されません
        ...
1      continue
      return
      end

この例では、サブルーチン自体が並列で実行されているので、その中のループは並列化されません。

例: ループから外部へのジャンプ


!$omp parallel do
      do i = 1, 1000     ! <- 並列化されず、エラーとなります
        ...
        if (a(i) .gt. min_threshold ) go to 20
        ...
      end do
20      continue
      ...

並列化のマークが付いたループの外にジャンプがあると、コンパイラはエラーと診断します。

例: ループ依存性を持つループの変数


demo% cat vpfn.f
      real function fn (n,x,y,z)
      real y(*),x(*),z(*)
      s = 0.0
!$omp parallel do private(i,s) shared(x,y,z)
      do  i = 1, n
          x(i) = s
          s = y(i)*z(i)
      enddo
      fn=x(10)
      return
      end
demo% f95 -c -vpara -loopinfo -openmp -O4 vpfn.f
"vpfn.f", line 5: Warning: the loop may have parallelization inhibiting reference
"vpfn.f", line 5: PARALLELIZED, user pragma used

ループは並列化されますが、可能なループ依存性は警告中で診断されます。しかし、ループ依存性のすべてがコンパイラによって診断できないことに注意してください。

10.3.1.4 明示的並列化での入出力

並列に実行するループで入出力を実行できます。 ただし、次の条件があります。

例: ループ内の入出力文


!$OMP PARALLEL DO PRIVATE(k)
      do i = 1, 10     !  並列化されます
        k = i
        call show ( k )
      end do
      end
      subroutine show( j )
      write(6,1) j
1      format(’Line number ’, i3, ’.’)
      end
demo% f95 -openmp t13.f
demo% setenv PARALLEL 4
demo% a.out

Line number 9.
Line number 4.
Line number 5.
Line number 6.
Line number 1.
Line number 2.
Line number 3.
Line number 7.
Line number 8.

ただし、入出力が再帰的な場合、つまり、入出力文に、入出力を行う関数への呼び出しが含まれている場合は、実行時エラーが発生します。

10.3.2 OpenMP 並列化指令

OpenMP は、マルチプロセッサプラットフォーム用の並列プログラミングモデルで、Fortran 95、C、C++ のアプリケーションの標準的なプログラミング方法となってきているものです。Forte Developer コンパイラでは、この並列プログラミングモデルを推奨しています。

OpenMP 指令を有効にするには、-openmp オプションフラグを使用してコンパイルします。Fortran 95 OpenMP 指令は、指令名と従属句の前に付く、コメントのような !$OMP という符号によって識別されます。

!$OMP PARALLEL は、プログラム内の並列領域を識別します。!$OMP DO は、並列領域内で並列化すべき DO ループを識別します。この 2 つの指令を組み合せて 1 つの !$OMP PARALLEL DO 指令とすることができます。 この指令は、DO ループの直前に配置します。

OpenMP の仕様には、プログラムの 1 つの並列領域内で作業を共有および同期化するための多数の指令と、データのスコープ指定および制御のための従属句が含まれています。

OpenMP 指令と古い Sun 形式の指令のもっとも大きな違いは、OpenMP では、非公開または共有のいずれかとして明示的にデータのスコープを指定する必要があることです。ただし、自動スコープ指定機能が提供されています。

Sun や Cray の並列化指令を使用して古いプログラムを変換するためのガイドラインも含めて、詳細は、Forte Developer の『OpenMP API ユーザーズガイド』を参照してください。

10.4 環境変数

並列化で使用される環境変数がいくつかあります。たとえば、OMP_NUM_THREADS、SUNW_MP_WARN、SUNW_MP_THR_IDLE、SUNW_MP_PROCBIND、STACKSIZE などです。これらについては、『OpenMP API ユーザーズガイド』に説明があります。

10.5 並列化されたプログラムをデバッグする

Fortran ソースコード:


    real x / 1.0 /, y / 0.0 /
    print *, x/y
    end
    character  string*5, out*20
    double precision value
    external exception_handler
    i = ieee_handler('set', 'all', exception_handler)
    string = '1e310'
    print *, '入力文字列', string, ' は: ', value
    print *, '1e300 * 1e10 の値は:', 1e300 * 1e10
    i = ieee_flags('clear', 'exception', 'all', out)
    end

    integer function exception_handler(sig, code, sigcontext)
    integer sig, code, sigcontext(5)
    print *, '*** IEEE 例外が発生しました!'
    return
    end

実行時出力:


*** IEEE 例外が発生しました!
 入力文字列 1e310 は: Infinity
  1e300 * 1e10 の値は: Inf
 注意: 次の IEEE 浮動小数点トラップが有効になっています;
  ieee_handler(3M) を参照:
 Inexact;  Underflow;  Overflow;  Division by Zero;  Invalid
   Operand;
 Sun の IEEE 演算の実装については、
『数値計算ガイド』を参照してください。

並列化されたプログラムをデバッグするには、新たな作業が必要になります。次に、その方法をいくつか示します。

10.5.1 デバッグの最初の手順

エラーの原因を特定するためにすぐに試してみることができる手順がいくつかあります。

これを


    DO I=1,N
      ...
      CALL SNUBBER(I)
      ...
    ENDDO

これに置き換える


      DO I1=1,N
      I=I1
      ...
      CALL SNUBBER(I)
      ...
    ENDDO

10.6 参考文献

次の参考文献には、さらに詳細な説明があります。