Fortran プログラミングガイド |
第 9 章
パフォーマンスと最適化
この章では、数値処理が多い Fortran プログラムのパフォーマンスを上げる可能性のある最適化のテクニックについて考えます。アルゴリズム、コンパイラオプション、ライブラリルーチン、コーディング技術を適切に使用することで、パフォーマンスを大幅に上げることができます。この章では、キャッシュ、入出力、システム環境のチューニングについては述べません。並列化の話題は、次の章で扱います。
- パフォーマンスを上げる可能性のあるコンパイラオプション
- 実行時パフォーマンスプロファイルからのフィードバックを使用したコンパイル
- 共通手続きの最適化されたライブラリルーチンの使用
- 重要なループのパフォーマンスを上げるためのコーディング戦略
最適化とパフォーマンスチューニングという問題は複雑すぎて、ここでそのすべてを扱うことはできません。しかし、この章を読んで、読者が少しでも上記の話題を知ってもらえればかまいません。この章の終わりに、この問題をより深く掘り下げて説明している書籍のリストを掲載しています。
最適化とパフォーマンスチューニングは、何を最適化するか、あるいは何をチューニングするかを決定できるかどうかに大きく依存する技法です。
コンパイラオプションの選択
適切なコンパイラオプションを選択することは、パフォーマンスを上げるための第一歩です。Sun コンパイラは、オブジェクトコードに影響する幅広いオプションを提供します。デフォルトの (コンパイルコマンド行にオプションを何も明示的に指定しない) 場合、ほとんどのオプションはオフです。パフォーマンスを上げるには、これらのオプションを明示的に選択しなければなりません。
パフォーマンスオプションは通常デフォルトではオフです。なぜなら、ほとんどの最適化によって、コンパイラは、ユーザーのソースコードについて仮定を行うからです。標準のコーディング技術に準拠し、隠れた副作用を発生させないプログラムは、正しく最適化できるはずです。しかし、標準の技術を恣意的に扱うプログラムは、コンパイラの仮定のいくつかと衝突する可能性があります。この結果作成されるコードは高速に実行するかもしれませんが、計算の結果は間違っている可能性があります。
推奨できる方法は、まず、すべてのオプションをオフにしてコンパイルし、計算の結果が正確であることを検証し、これらの最初の結果とパフォーマンスプロファイルをベースラインとして使用する方法です。それから、実行結果とパフォーマンスをベースラインと比較しながら、段階的に、オプションを追加してコンパイルし直します。数値結果が変わるようであれば、そのプログラムには疑わしいコードがあるといえます。注意深く、その問題がどこにあるのかを解析して、プログラムし直す必要があります。
最適化オプションを追加した結果、パフォーマンスがあまり上がらない (あるいは下がってしまった) 場合、そのコーディングにはコンパイラがパフォーマンスを上げる余地がないのかもしれません。次の段階は、プログラムをソースコードレベルで解析し、構造を変形することによって、パフォーマンスを上げることです。
パフォーマンスオプションのリファレンス
次の表にリストしたコンパイラオプションによって、デフォルトのコンパイルで作成されるプログラムのパフォーマンスを上げるための方法のレパートリーは広がります。このリストには、コンパイラの中でもよりパフォーマンスに影響を与えるオプションだけを紹介しました。完全なリストについては、『Fortran ユーザーズガイド』を参照してください。
このようなオプションはコンパイル時間を増やすものもあります。なぜなら、プログラムをより深く解析するからです。オプションの中には、呼び出すルーチンと呼び出されるルーチンを同じファイルに集めておくと (それぞれを別々なファイルに入れておくよりも) うまく動作するものもあります。これによって、解析が大域的に行われるからです。
-fast
このオプション 1 つで、いくつものパフォーマンスオプションを選択したことになります。これらのオプションはいっしょに働き、実行速度重視で最適化されたオブジェクトコードを生成します。コンパイル時間もそれほど増えません。
-fast
で選択されるオプションは、リリースによって異なり、各プラットフォームですべて利用できるわけではありません。
native
。ホストアーキテクチャ用に最適化されたコードを生成します。-O5
。最適化レベルを設定します。-libmi1
。いくつかの簡単なライブラリ関数の呼び出しをインライン化します。-fsimple=2
。浮動小数点コードの簡易化を行います。-dalign
。より高速なダブルワードのロードとストアを使用します。-xlibmopt。libm
数学ライブラリの最適化されたバージョンを呼び出します。-fns -ftrap=%none
。すべてのトラップをオフにします。-ftrap=%none
によりf77
のすべてのトラップをオフにするか、または-ftrap=common
によりf95
の共通浮動小数点トラップを選択します。-depend
。データの依存関係についてのループの構造の解析を行います。-pad=common
を使用すると、キャッシュのパフォーマンスがよくなります。-xvector=yes
により、ベクトル化されたライブラリ関数がループ処理で起動されます。
-fast
は、コンパイラの最適化能力のほとんどを簡単に引き出すための方法です。複合オプションは個別にも指定できます。また、それぞれに注意すべき副作用があります (『Fortran ユーザーズガイド』を参照)。-fast
の後に別のオプションを追加して、さらに最適化を指定できます。たとえば、次のようにします。
注 --fast
には、-dalign
と-native
が含まれます。プログラムによっては、これらのオプションが予期しない副作用を起こすこともあります。
-O
n
-O
オプションを明示的に (あるいは、-fast
などのマクロオプションで暗黙的に) 指定しない限り、コンパイラは最適化を行いません。ほとんどすべての場合、コンパイルの最適化レベルを指定すると、プログラムの実行パフォーマンスは上がります。一方、最適化レベルを上げるほど、コンパイル時間が増え、コードのサイズも大きくなる可能性があります。ほとんどの場合、パフォーマンス、コードのサイズ、コンパイル時間を最もバランスよくコンパイルするのはレベル
-O3
です。レベル-O4
は、呼び出し側と同じソースファイルに入っているルーチンの呼び出しの自動インライン化を追加します。(サブプログラム呼び出しのインライン化の詳細については、『Fortran ユーザーズガイド』を参照してください。) レベル-O5
は、低いレベルには適用できない、さらに積極的な最適化テクニックを追加します。一般的に、-O3
より上のレベルは、プログラム中で最も計算が多い、つまりパフォーマンスが上がる見込みが大きい部分のルーチンだけに指定するものです。ちなみに、異なる最適化レベルでコンパイルしたプログラムをいっしょにリンクしても何の問題もありません。
PRAGMA OPT=
n
C$ PRAGMA SUN OPT=
n 指令を使用して、ソースファイルのルーチンごとに異なる最適化レベルを設定します。この指令はコンパイラのコマンド行の-O
n フラグに優先しますが、-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 倍精度のデータをすべて (REAL
もCOMPLEX
も) ダブルワード境界に揃えようとします。その結果、次のようなことが起こります。
- パディングを追加したために、
COMMON
ブロックが予想よりも大きくなることがあります。COMMON
を共有するプログラム単位のいずれか 1 つでも-dalign
を付けてコンパイルした場合、すべての単位を-dalign
を付けてコンパイルしなければなりません。たとえば、複数のデータ型が混在する
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 まで展開する様子を示しています (このオプションを使用しても、ソースコードは変更されません)。
この例は、固定した繰り返しの簡単なループを示しています。可変の繰り返し数を持つループに対しては、構造の変更はもっと複雑になります。
-xtarget=
platformコンパイラにターゲットのコンピュータハードウェアの正確な情報を伝えると、パフォーマンスが上がるプログラムもあります。プログラムパフォーマンスが重要なとき、ターゲットハードウェアを適切に指定することは非常に重要な問題となります。特に、新しい SPARC プロセッサ上で実行する場合です。しかし、ほとんどのプログラムと古い SPARC プロセッサの場合、パフォーマンスはそれほど上がらず、汎用指定だけで十分です。
『Fortran ユーザーズガイド』には、
-xtarget=
が認識するすべてのシステム名をリストしています。特定のシステム名に対して (たとえば、UltraSPARK IITM ならultra2
)、-xtarget
は、システムに適切に一致するように、-xarch、-xcache、
の組み合わせに展開されます。オプティマイザはこれらの指定を使用して、従うべき方法と生成する命令を決定します。
-xchip
-xtarget=native
は特別な設定で、これを指定すると、オプティマイザはホストシステム (コンパイルを行うシステム) をターゲットとしてコードをコンパイルします。コンパイルと実行を同じシステム上で行うときは、このオプションが断然便利です。実行システムが不明であるときは、汎用のアーキテクチャ用にコンパイルするのが望ましい方法です。そのため、最適のパフォーマンスを得ることはできませんが、-xtarget=generic
がデフォルトになります。パフォーマンスに関するその他の方針
さまざまな最適化オプションを使用し、プログラムをコンパイルし、実際の実行時パフォーマンスを測定したと仮定します。次の段階は、Fortran ソースプログラムを調べて、さらにチューニングできるかどうかを決定します。
計算時間のほとんどを消費するプログラムの部分だけに注目し、次の方針を考えます。
- 手作業で作成した手続きを、最適化された同等のライブラリへの呼び出しに置き換える。
- 重要なループから入出力、呼び出し、不必要な条件操作を削除する。
- 最適化を抑制する可能性がある別名を削除する。
- ブロック
IF
を使用して、複雑にからまったコードを合理化する。上記は、パフォーマンスを上げる可能性を持つプログラミング技術の一例です。さらに、特定のハードウェア構成にあわせて手作業でソースコードを調整することもできます。しかし、このような作業はコードをわかりにくくするだけでなく、コンパイラのオプティマイザもパフォーマンスを上げにくくなります。手作業でソースコードをチューニングしすぎると、その手続きの本来の意図が隠され、異なるアーキテクチャ上ではパフォーマンスに重大な悪影響を与えかねません。
最適化されたライブラリの使用
ほとんどの場合、商業用 (あるいはシェアウェア) の最適化されたライブラリは、ユーザーが手作業でコーディングしたものよりも、はるかに効率的に標準の計算手続きを実行します。
たとえば、Sun Performance Library` は、標準の LAPACK、BLAS、FFTPACK、VFFTPACK、LINPACK ライブラリをベースとした数学サブルーチンで、高度に最適化されています。このライブラリのルーチンを使用すると、パフォーマンスは手作業でコーディングしたときよりも大幅に上がります。
パフォーマンスの抑制要因を削除する
Sun WorkShop パフォーマンス解析を使用して、プログラムの重要な計算部分を調べます。そして、注意深くループまたはループの入れ子を解析し、オプティマイザが最適なコードを生成するのを抑制している、つまりパフォーマンスを下げているコーディングを削除します。標準以外のコーディングが多いと、移植が困難になり、さらにはコンパイラによる最適化を抑制する可能性があります。
パフォーマンスを上げるためのプログラムの書き直しテクニックに関しては、この章の最後に紹介する、さまざまな参考文献で取り上げられています。ここでは 3 つの代表的なアプローチを説明します。
キーとなるループから入出力を削除する
プログラムの重要な計算作業を囲んでいるループ、あるいはループの入れ子内の入出力は、パフォーマンスを大幅に下げる原因となります。入出力ライブラリで消費される CPU 時間は、そのループで消費される時間のほとんどを占めます (入出力はまたプロセス割り込みの原因ともなるので、プログラムスループットを下げます)。可能な限り、入出力を計算ループの外に出すことで、入出力ライブラリへの呼び出し回数が大幅に減ります。
副プログラムの呼び出しを削除する
副プログラムがループの深い入れ子から呼び出されると、何千回と呼び出される可能性もあります。呼び出しごとの各ルーチン内で消費される時間は少なくても、その合計の影響はかなりのものです。また、副プログラムの呼び出しは、その呼び出しを含むループの最適化を抑制します。なぜなら、コンパイラは、その呼び出しのレジスタの状態に関して仮定を行うことができないからです。
副プログラム呼び出しの自動インライン化 (
-inline=
x,y,..x、または-O4
を使用する) は、コンパイラが実際の呼び出しを副プログラム自身で置き換える (副プログラムをループの中に入れる) ための 1 つの方法です。 インライン化されるべきルーチンの副プログラムのソースコードは、呼び出し側のルーチンと同じファイルに存在しなければなりません。
- 文関数を使用する。呼び出される外部関数が単純な数学関数である場合、その関数を文関数 (あるいは文関数の集合) として書き直すことができます。文関数はコンパイル時にインライン化され、最適化できます。
- ループを副プログラムに入れる。つまり、副プログラムを書き換えて、 (ループの外で) 呼び出される回数を減らし、呼び出しごとに値のベクトルあるいは配列を操作するようにします。
複雑にからまったコードを合理化する
計算が多いループ内の操作が複雑であると、コンパイラの最適化は抑制される可能性があります。一般的に、算術的な
IF
と論理的なIF
をすべてブロックIF
に置き換えるのがよい方法であるとされています。
ブロック
IF
を使用すると、コンパイラが最適なコードを生成する機会が多くなるだけでなく、読みやすくなるので、移植性も確保されます。参考文献
サン・マイクロシステムズ株式会社 Copyright information. All rights reserved. |
ホーム | 目次 | 前ページへ | 次ページへ | 索引 |