Fortran プログラミングガイド ホーム目次前ページへ次ページへ索引


第 9 章

パフォーマンスと最適化

この章では、数値処理が多い Fortran プログラムのパフォーマンスを上げる可能性のある最適化のテクニックについて考えます。アルゴリズム、コンパイラオプション、ライブラリルーチン、コーディング技術を適切に使用することで、パフォーマンスを大幅に上げることができます。この章では、キャッシュ、入出力、システム環境のチューニングについては述べません。並列化の話題は、次の章で扱います。

この章では、次の話題を取り上げます。

最適化とパフォーマンスチューニングという問題は複雑すぎて、ここでそのすべてを扱うことはできません。しかし、この章を読んで、読者が少しでも上記の話題を知ってもらえればかまいません。この章の終わりに、この問題をより深く掘り下げて説明している書籍のリストを掲載しています。

最適化とパフォーマンスチューニングは、何を最適化するか、あるいは何をチューニングするかを決定できるかどうかに大きく依存する技法です。

コンパイラオプションの選択

適切なコンパイラオプションを選択することは、パフォーマンスを上げるための第一歩です。Sun コンパイラは、オブジェクトコードに影響する幅広いオプションを提供します。デフォルトの (コンパイルコマンド行にオプションを何も明示的に指定しない) 場合、ほとんどのオプションはオフです。パフォーマンスを上げるには、これらのオプションを明示的に選択しなければなりません。

パフォーマンスオプションは通常デフォルトではオフです。なぜなら、ほとんどの最適化によって、コンパイラは、ユーザーのソースコードについて仮定を行うからです。標準のコーディング技術に準拠し、隠れた副作用を発生させないプログラムは、正しく最適化できるはずです。しかし、標準の技術を恣意的に扱うプログラムは、コンパイラの仮定のいくつかと衝突する可能性があります。この結果作成されるコードは高速に実行するかもしれませんが、計算の結果は間違っている可能性があります。

推奨できる方法は、まず、すべてのオプションをオフにしてコンパイルし、計算の結果が正確であることを検証し、これらの最初の結果とパフォーマンスプロファイルをベースラインとして使用する方法です。それから、実行結果とパフォーマンスをベースラインと比較しながら、段階的に、オプションを追加してコンパイルし直します。数値結果が変わるようであれば、そのプログラムには疑わしいコードがあるといえます。注意深く、その問題がどこにあるのかを解析して、プログラムし直す必要があります。

最適化オプションを追加した結果、パフォーマンスがあまり上がらない (あるいは下がってしまった) 場合、そのコーディングにはコンパイラがパフォーマンスを上げる余地がないのかもしれません。次の段階は、プログラムをソースコードレベルで解析し、構造を変形することによって、パフォーマンスを上げることです。

パフォーマンスオプションのリファレンス

次の表にリストしたコンパイラオプションによって、デフォルトのコンパイルで作成されるプログラムのパフォーマンスを上げるための方法のレパートリーは広がります。このリストには、コンパイラの中でもよりパフォーマンスに影響を与えるオプションだけを紹介しました。完全なリストについては、『Fortran ユーザーズガイド』を参照してください。

表 9-1   パフォーマンスに影響を与えるオプション
動作 オプション
さまざまな最適化オプションをいっしょに使用する -fast
コンパイラの最適化レベルを n に設定する -On (-O = -O3)
ターゲットハードウェアを指定する -xtarget=sys
パフォーマンスプロファイルデータを使用して最適化する (-O5 で) -xprofile=use
ループを n まで展開する -unroll=n
浮動小数点の簡約化と最適化を許可する -fsimple=1|2
依存関係の解析を行い、ループを最適化する -depend


このようなオプションはコンパイル時間を増やすものもあります。なぜなら、プログラムをより深く解析するからです。オプションの中には、呼び出すルーチンと呼び出されるルーチンを同じファイルに集めておくと (それぞれを別々なファイルに入れておくよりも) うまく動作するものもあります。これによって、解析が大域的に行われるからです。

-fast

このオプション 1 つで、いくつものパフォーマンスオプションを選択したことになります。これらのオプションはいっしょに働き、実行速度重視で最適化されたオブジェクトコードを生成します。コンパイル時間もそれほど増えません。

-fast で選択されるオプションは、リリースによって異なり、各プラットフォームですべて利用できるわけではありません。

-fast は、コンパイラの最適化能力のほとんどを簡単に引き出すための方法です。複合オプションは個別にも指定できます。また、それぞれに注意すべき副作用があります (『Fortran ユーザーズガイド』を参照)。-fast の後に別のオプションを追加して、さらに最適化を指定できます。たとえば、次のようにします。

f95 -fast -xarch=v9a ...

すると、最適化レベルが 4 ではなく 5 になります。


注 - -fast には、-dalign-native が含まれます。プログラムによっては、これらのオプションが予期しない副作用を起こすこともあります。

-On

-O オプションを明示的に (あるいは、-fast などのマクロオプションで暗黙的に) 指定しない限り、コンパイラは最適化を行いません。ほとんどすべての場合、コンパイルの最適化レベルを指定すると、プログラムの実行パフォーマンスは上がります。一方、最適化レベルを上げるほど、コンパイル時間が増え、コードのサイズも大きくなる可能性があります。

ほとんどの場合、パフォーマンス、コードのサイズ、コンパイル時間を最もバランスよくコンパイルするのはレベル -O3 です。レベル -O4 は、呼び出し側と同じソースファイルに入っているルーチンの呼び出しの自動インライン化を追加します。(サブプログラム呼び出しのインライン化の詳細については、『Fortran ユーザーズガイド』を参照してください。) レベル -O5 は、低いレベルには適用できない、さらに積極的な最適化テクニックを追加します。一般的に、-O3 より上のレベルは、プログラム中で最も計算が多い、つまりパフォーマンスが上がる見込みが大きい部分のルーチンだけに指定するものです。ちなみに、異なる最適化レベルでコンパイルしたプログラムをいっしょにリンクしても何の問題もありません。

PRAGMA OPT=n

C$ PRAGMA SUN OPT=n 指令を使用して、ソースファイルのルーチンごとに異なる最適化レベルを設定します。この指令はコンパイラのコマンド行の -On フラグに優先しますが、-xmaxopt=n フラグで最大最適化レベルを設定して使用しなければなりません。詳細は、f77(1) と f95(1) のマニュアルページを参照してください。

実行時プロファイルのフィードバックを使用した最適化

-xprofile=use と組み合わせた場合、コンパイラはレベル -O3 以上の最適化をより効率的に適用します。このオプションを使用すると、オプティマイザは、
-xprofile=collect でコンパイルしたプログラムが典型的な入力データを使用して生成した実行時実行プロファイルから指示を受けます。フィードバックプロファイルは、どこで最適化が最大の効果を発揮するかをコンパイラに示します。これは特に -O5 で重要になります。次に示す例は、より高い最適化レベルでプロファイルを収集する典型的な例です。

demo% f95 -o prg -fast -xprofile=collect prg.f ...
demo% prg    
demo% f95 -o prgx -fast -O5 -xprofile=use:prg.profile prg.f ...
demo% prgx  

上記の例の最初のコンパイルで、実行時に文カバレージ統計を生成する実行可能ファイルが生成されます。2 回目のコンパイルで、このパフォーマンスデータを使用して、プログラムを最適化しています。

-xprofile オプションに関する詳細は、『Fortran ユーザーズガイド』を参照してください。

-dalign

-dalign を使用すると、コンパイラはダブルワードのロード命令またはストア命令を (可能であれば) 生成できます。データの移動量が多いプログラムは、このオプションを付けてコンパイルすれば、その恩恵を十分に受けることができます。-dalign は、-fast によって選択されるオプションの 1 つです。ダブルワード命令の速度は、同等のシングルワード命令と比べると、ほとんど倍になります。

しかし、-dalign を (つまり -fast も) 使用するときは十分注意しなければなりません。なぜなら、COMMON ブロック中のデータの特定の境界合わせを予想してコーディングされたプログラムのうち、問題を起こすものがあるからです。-dalign を使用すると、コンパイラはパディングを追加して、倍精度と 4 倍精度のデータをすべて (REALCOMPLEX も) ダブルワード境界に揃えようとします。その結果、次のようなことが起こります。

たとえば、複数のデータ型が混在する COMMON ブロック全体を 1 つの配列として別名付けを行うことによって、データを書き込むプログラムは -dalign を付けるとうまく動作しません。なぜなら、倍精度変数や 4 倍精度変数のパディングのために、プログラムが予想するよりもブロックが大きくなるからです。

-depend

(SPARC プラットフォーム上で) 最適化レベル -O3 以上に -depend を追加すると、DO ループとループの入れ子に関するコンパイラの最適化能力が拡張されます。このオプションを使用すると、オプティマイザはループの反復の間の依存関係を解析し、そのループ構造を変形できるかどうか決定します。依存関係のないループだけがその構造を変形できます。しかし、この解析を追加すると、コンパイル時間が増えます。

-fsimple=2

指示しない限り、コンパイラは浮動小数点計算を簡易化しようとしません (デフォルトは -fsimple=0)。-fast オプションを使用すると、-fsimple=1 が使用され、いくつかの保守的な仮定が行われます。-fsimple=2 を追加すると、オプティマイザはさらに簡易化を行うことができます。しかし、簡易化を行うと、丸めの影響によって、結果がわずかに違うという問題が発生する可能性があります。-fsimple レベル 1 か 2 を使用する場合は、すべてのプログラム単位を同じようにコンパイルし、数値精度の整合性が失われないようにしなければなりません。

-unroll=n

長い繰り返しを持つ短いループを展開すると、いくつかのルーチンはその恩恵を受けることがあります。しかし、展開はプログラムのサイズを増やすことにもなり、他のループのパフォーマンスを下げることにもなります。n=1 を使用すると (デフォルト)、オプティマイザは自動的にループを展開しません。n が 1 より大きいときは、オプティマイザは、深さが n までループを展開しようとします。

コンパイラのコードジェネレータはループの展開をさまざまな要因に応じて決定します。コンパイラは、オプションが n>1 で指定されている場合でもループの展開を縮小することがあります。

繰り返しが可変の DO ループを展開する場合、展開したループとオリジナルのループの両方がコンパイルされます。繰り返しを実行時にテストして、展開したループを実行するのが適切かどうかを決定します。ループを展開すると、特に文が 1 つか 2 つしかないループの場合は、反復ごとに行われる計算量が増えるので、オプティマイザがレジスタをスケジュールし演算を単純化する機会が増えます。繰り返しの数、ループの複雑さ、展開の深さの選択のかね合いは簡単に決定できず、ある程度の経験が必要となるでしょう。

次に示す例は、-unroll=4 を指定して、簡単なループを深さが 4 まで展開する様子を示しています (このオプションを使用しても、ソースコードは変更されません)。

元のループ:
    DO I=1,20000
       X(I) = X(I) + Y(I)*A(I)
    END DO

 
書き込みがあったかのように 4 で展開すると:
    DO I=1, 19997,4
       TEMP1 = X(I) + Y(I)*A(I)
       TEMP2 = X(I+1) + Y(I+1)*A(I+1)
       TEMP3 = X(I+2) + Y(I+2)*A(I+2)
       X(I+3) = X(I+3) + Y(I+3)*A(I+3)
       X(I) = TEMP1
       X(I+1) = TEMP2
       X(I+2) = TEMP3
    END DO

この例は、固定した繰り返しの簡単なループを示しています。可変の繰り返し数を持つループに対しては、構造の変更はもっと複雑になります。

-xtarget=platform

コンパイラにターゲットのコンピュータハードウェアの正確な情報を伝えると、パフォーマンスが上がるプログラムもあります。プログラムパフォーマンスが重要なとき、ターゲットハードウェアを適切に指定することは非常に重要な問題となります。特に、新しい SPARC プロセッサ上で実行する場合です。しかし、ほとんどのプログラムと古い SPARC プロセッサの場合、パフォーマンスはそれほど上がらず、汎用指定だけで十分です。

『Fortran ユーザーズガイド』には、-xtarget= が認識するすべてのシステム名をリストしています。特定のシステム名に対して (たとえば、UltraSPARK IITM なら ultra2)、-xtarget は、システムに適切に一致するように、-xarch、-xcache、
-xchip
の組み合わせに展開されます。オプティマイザはこれらの指定を使用して、従うべき方法と生成する命令を決定します。

-xtarget=native は特別な設定で、これを指定すると、オプティマイザはホストシステム (コンパイルを行うシステム) をターゲットとしてコードをコンパイルします。コンパイルと実行を同じシステム上で行うときは、このオプションが断然便利です。実行システムが不明であるときは、汎用のアーキテクチャ用にコンパイルするのが望ましい方法です。そのため、最適のパフォーマンスを得ることはできませんが、-xtarget=generic がデフォルトになります。

パフォーマンスに関するその他の方針

さまざまな最適化オプションを使用し、プログラムをコンパイルし、実際の実行時パフォーマンスを測定したと仮定します。次の段階は、Fortran ソースプログラムを調べて、さらにチューニングできるかどうかを決定します。

計算時間のほとんどを消費するプログラムの部分だけに注目し、次の方針を考えます。

上記は、パフォーマンスを上げる可能性を持つプログラミング技術の一例です。さらに、特定のハードウェア構成にあわせて手作業でソースコードを調整することもできます。しかし、このような作業はコードをわかりにくくするだけでなく、コンパイラのオプティマイザもパフォーマンスを上げにくくなります。手作業でソースコードをチューニングしすぎると、その手続きの本来の意図が隠され、異なるアーキテクチャ上ではパフォーマンスに重大な悪影響を与えかねません。

最適化されたライブラリの使用

ほとんどの場合、商業用 (あるいはシェアウェア) の最適化されたライブラリは、ユーザーが手作業でコーディングしたものよりも、はるかに効率的に標準の計算手続きを実行します。

たとえば、Sun Performance Library` は、標準の LAPACK、BLAS、FFTPACK、VFFTPACK、LINPACK ライブラリをベースとした数学サブルーチンで、高度に最適化されています。このライブラリのルーチンを使用すると、パフォーマンスは手作業でコーディングしたときよりも大幅に上がります。

パフォーマンスの抑制要因を削除する

Sun WorkShop パフォーマンス解析を使用して、プログラムの重要な計算部分を調べます。そして、注意深くループまたはループの入れ子を解析し、オプティマイザが最適なコードを生成するのを抑制している、つまりパフォーマンスを下げているコーディングを削除します。標準以外のコーディングが多いと、移植が困難になり、さらにはコンパイラによる最適化を抑制する可能性があります。

パフォーマンスを上げるためのプログラムの書き直しテクニックに関しては、この章の最後に紹介する、さまざまな参考文献で取り上げられています。ここでは 3 つの代表的なアプローチを説明します。

キーとなるループから入出力を削除する

プログラムの重要な計算作業を囲んでいるループ、あるいはループの入れ子内の入出力は、パフォーマンスを大幅に下げる原因となります。入出力ライブラリで消費される CPU 時間は、そのループで消費される時間のほとんどを占めます (入出力はまたプロセス割り込みの原因ともなるので、プログラムスループットを下げます)。可能な限り、入出力を計算ループの外に出すことで、入出力ライブラリへの呼び出し回数が大幅に減ります。

副プログラムの呼び出しを削除する

副プログラムがループの深い入れ子から呼び出されると、何千回と呼び出される可能性もあります。呼び出しごとの各ルーチン内で消費される時間は少なくても、その合計の影響はかなりのものです。また、副プログラムの呼び出しは、その呼び出しを含むループの最適化を抑制します。なぜなら、コンパイラは、その呼び出しのレジスタの状態に関して仮定を行うことができないからです。

副プログラム呼び出しの自動インライン化 (-inline=x,y,..x、または -O4 を使用する) は、コンパイラが実際の呼び出しを副プログラム自身で置き換える (副プログラムをループの中に入れる) ための 1 つの方法です。 インライン化されるべきルーチンの副プログラムのソースコードは、呼び出し側のルーチンと同じファイルに存在しなければなりません。

副プログラム呼び出しを削除する方法は他にもあります。

複雑にからまったコードを合理化する

計算が多いループ内の操作が複雑であると、コンパイラの最適化は抑制される可能性があります。一般的に、算術的な IF と論理的な IF をすべてブロック IF に置き換えるのがよい方法であるとされています。

元のコード:
    IF(A(I)-DELTA) 10,10,11
10  XA(I) = XB(I)*B(I,I)
    XY(I) = XA(I) - A(I)
    GOTO 13
11  XA(I) = Z(I)
    XY(I) = Z(I)
    IF(QZDATA.LT.0.) GOTO 12
    ICNT = ICNT + 1
    ROX(ICNT) = XA(I)-DELTA/2.
12  SUM = SUM + X(I)
13  SUM = SUM + XA(I)

 
整理されたコード:
    IF(A(I).LE.DELTA) THEN
      XA(I) = XB(I)*B(I,I)
      XY(I) = XA(I) - A(I)
    ELSE
      XA(I) = Z(I)
      XY(I) = Z(I)
      IF(QZDATA.GE.0.) THEN
        ICNT = ICNT + 1
        ROX(ICNT) = XA(I)-DELTA/2.
      ENDIF
      SUM = SUM + X(I)
    ENDIF
    SUM = SUM + XA(I)

ブロック IF を使用すると、コンパイラが最適なコードを生成する機会が多くなるだけでなく、読みやすくなるので、移植性も確保されます。

参考文献

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


サン・マイクロシステムズ株式会社
Copyright information. All rights reserved.
ホーム   |   目次   |   前ページへ   |   次ページへ   |   索引