Fortranの移植性
目次
Fortranの移植性
FortranはISOとJISで標準化され、処理系依存の部分が少ないように思われますが、ソースコードを他の処理系に移植すると、しばしば問題が発生します。本文書では、Fortranソースコードの移植性を上げるTipsを紹介します。
潜在的なバグの検出
ソースコードに潜在的なバグがあると、あるシステムでは一見正しく動いていても、移植するとバグが表面化することがあります。また、直列実行ではうまく動いていても、並列化するとバグが表面化することもあります。移植、並列化する前に、潜在的なバグを取り除くべきです。
Fortranのコードに最も多い潜在的なバグは、配列の領域外参照です。 Intelコンパイラーでは「-CB」、 PGIコンパイラーでは「-C」オプションを付けてコンパイルすると、実行時に配列の領域外参照が検出されます。
Intelコンパイラーのバージョン9.1からでは、「-check uninit」オプションによって、変数の初期化漏れが実行時に検出されます。ただし、配列と構造体の初期化漏れは検出されないようです。
Intelコンパイラーでは、「-check all」オプションを付けてコンパイルすると、可能な全ての実行時診断機能が働くので、潜在的なバグを見つけやすくなります。
浮動小数点演算例外の検出
ゼロ除算等の浮動小数点演算例外が発生した場合の動作は、処理系とコンパイラーオプションによって変わります。多くの処理系のデフォルトの動作では、例外が起きても計算が続くので、例外を見落としがちです。 Intelコンパイラーのでは「-fpe0」、 PGIコンパイラーでは「-Ktrap=fp」オプションを付けてコンパイルすると、浮動小数点演算例外が発生した場合に、エラーメッセージと共にプログラムがアボートされます。
Intelコンパイラーでは、異常終了時にソースコードの行番号を表示するための、「-traceback」オプションも指定してください。このオプションは、実行速度にほとんど影響しないので、常時指定をお勧めします。
sample/hello> cat fpe.f program fpe implicit none write(*,*) sqrt(-1.0d+0) stop end program fpe sample/hello> ifort -traceback fpe.f sample/hello> ./a.out NaN sample/hello> ifort -fpe0 -traceback fpe.f sample/hello> ./a.out forrtl: error (65): floating invalid Image PC Routine Line Source a.out 4000000000002B61 MAIN__ 4 fpe.f a.out 4000000000002A50 Unknown Unknown Unknown libc.so.6.1 2000000000475C50 Unknown Unknown Unknown a.out 4000000000002840 Unknown Unknown Unknown アボート
MPIライブラリーのインターフェース
MPIを使うFortranコードでは、しばしば、「include \"mpif.h\"」と書いて、パラメーター文による定数宣言を読み込みます。しかし、「use mpi」と書くほうが、ライブラリーのインターフェース宣言も読み込まれ、引数の不整合がコンパイル時に発見されるので、望ましいです。
「use」文はプログラム単位を宣言する文と「implicit」文の間にのみ書けるので、しばしば、次のようなソースコードになります。
subroutine foo(args) use mpi implicit none
例を示します。次のコードは、ライブラリーサブルーチン「MPI_Init」の引数が不足しています。
program hellompi use mpi implicit none integer :: numrank, myrank, ierr call MPI_Init call MPI_Comm_size(MPI_COMM_WORLD, numrank, ierr) call MPI_Comm_rank(MPI_COMM_WORLD, myrank, ierr) write(*,*) \"Hello, MPI. rank=\", myrank, numrank call MPI_Finalize(ierr) stop end
コンパイルすると、次のようにエラーになります。
sample/hello> ifort hellompi.f -lmpi fortcom: Error: hellompi.f, line 6: A non-optional actual argument must be prese nt when invoking a procedure with an explicit interface. [IERROR] call MPI_Init -----------^ compilation aborted for hellompi.f (code 1)
暗黙の型宣言
暗黙の型宣言は、災いの元です。プログラム単位の始めに、「implicit none」と書きましょう。
Intelコンパイラーでは「-warn declarations」、 PGIコンパイラーでは「-Mdclchk」オプションを付けてコンパイルすると、暗黙の型宣言が警告されます。
次のコードは、「MPI_COMM_WORLD」と書くべきところを、「MPI_BUG_WORLD」と書いていますが、暗黙の型宣言が有効であると、コンパイルできてしまいます。
program hellompi use mpi integer :: numrank, myrank, ierr call MPI_Init(ierr) call MPI_Comm_size(MPI_BUG_WORLD, numrank, ierr) call MPI_Comm_rank(MPI_BUG_WORLD, myrank, ierr) write(*,*) \"Hello, MPI. rank=\", myrank, numrank call MPI_Finalize(ierr) stop end program hellompi
しかも、コンパイラーオプションによっては、実行できるけれど結果がおかしいという、最悪の事態になります。
sample/hello> ifort -save -zero implicit_bug.f -lmpi sample/hello> mpijob -np 2 ./a.out LSB_MCPU_HOSTS:jaerif 2 mpirun -np 2 ./a.out Hello, MPI. rank= 0 0 Hello, MPI. rank= 0 0
コンパイラーオプションによって、暗黙の型宣言を探してみます。
sample/hello> ifort -save -zero -warn declarations implicit_bug.f -lmpi fortcom: Warning: implicit_bug.f, line 6: This name has not been given an explic it type. [MPI_BUG_WORLD] call MPI_Comm_size(MPI_BUG_WORLD, numrank, ierr) -------------------------^
標準外機能の利用
Fortranコンパイラーには、ISO/JISの標準を超える各種の拡張機能があります。便利ではありますが、移植性を損なう原因にもなります。
Intelコンパイラーでは「-std」オプション、 PGIコンパイラーでは「-Mstandard」オプションを付けてコンパイルすると、標準外の機能の利用が警告されます。
メモリの配置と初期化
C言語では、「static」宣言された変数と広域変数は静的に、「auto」変数はスタックまたはレジスターに、「malloc」で確保された領域はヒープに置かれると、最初から決まっていました。しかし、Fortran言語では、当初静的なメモリ配置しかなかったので、メモリの配置方法の指定が後付けになってしまいました。「save」属性が指定された変数、共通ブロック、モジュールの広域変数、DATA文で初期化される変数は、必ず静的に配置されます。再帰的プログラム単位の局所変数、実行時に大きさが指定される配列は、必ず動的に配置されます。残りの変数は、コンパイラーオプションによって、配置方法が変わります。
Intelコンパイラーの場合、オプション依存の変数は、「-auto」オプションによって動的に、「-save」オプションによって静的に、「-auto_scalar」オプションによって単純変数は動的に配列変数と構造体変数は静的に配置されます。さらに、「-zero」オプションを付けると、静的に配置される変数が0で初期化され、伝統的な処理系との互換性がよくなります。 \'\'注意:動的に配置される変数は初期化されません。\'\' また、「-ftrapuv」オプションを付けると、動的に配置される変数が、ポインターとして使うとセグメンテーションフォールトを起こすような値で、具体的には0xccccccccで初期化され、初期化漏れによるバグを洗い出すのに役立ちます。
Intelコンパイラーの通常のデフォルトは「-auto_scalar」ですが、「-oenmp」を指定すると、「-auto」がデフォルトになります。 OpenMPディレクティブが正しいはずなのに、OpenMPで計算が合わない場合には、明示的な「-auto_scalar」オプションを試してください。
次のコードは、1から10までの和を求めますが、変数の初期化漏れがあります。
program uninit implicit none integer :: i, s do i = 1, 10 s = s + i enddo write(*,*) s stop end program uninit
sample/hello> ifort uninit.f sample/hello> ./a.out 55 sample/hello> ifort -save -zero uninit.f sample/hello> ./a.out 55 sample/hello> ifort -auto -ftrapuv uninit.f sample/hello> ./a.out -858993405
コンパイラーオプション「-save -zero」の場合と、「-auto -ftrapuv」の場合とで、計算結果が異なるので、変数の初期化漏れがあるらしいと判ります。
PGIコンパイラーの場合、オプション依存の変数は、「-Msave」オプションによって静的に、「-Mnosave」オプションによって動的に配置されます。
メモリーを節約するために、共通ブロックで変数を使いまわすと、コードの保守性が悪くなります。現代の動的メモリー管理が可能な処理系では、本当に共有する必要がある変数だけを、共通ブロックまたはモジュール変数にしてください。副プログラムを終了しても値を保存する必要がある局所変数は、共通ブロックに入れるのではなく、「SAVE」属性を指定してください。例:
integer(8), save :: random_seed
数値的安定性、特にカオスの問題
数値解析の問題には、数値的に安定なものと不安定なものがあります。数値的に不安定な問題は、処理系によって計算結果が変わったり、並列化によって計算結果が変わったりすることがあります。例えば、粒子の運動の問題は、決定論的カオス現象を起こし、数値的に不安定なので、統計的に精度を評価する必要があります。
数値的安定性を評価するために、最も単純な方法は、単精度計算と倍精度計算の結果の比較です。両者の結果が十進数で6桁程度一致していれば、その問題は数値的に安定で、計算結果を信用できます。両者の結果が全く異なれば、その問題は数値的に不安定で、計算結果の妥当性を慎重に吟味する必要があります。
決定論的カオス現象を起こすコードの例です。解析力学の教科書にしばしば述べられている例です。乱数を使っておらず、何度実行しても同じ結果が出るので、「決定論的カオス」といいます。
program chaos implicit none integer :: iii real :: xxx xxx = 1.0 / 3.0 do iii = 1, 20 xxx = 4.0 * (1.0 - xxx) * xxx write(*,\'(i3, f12.7)\') iii, xxx enddo stop end program chaos
単精度計算(左)と倍精度計算(右)の結果を比較します。
1 0.8888888 1 0.8888889 2 0.3950619 2 0.3950617 3 0.9559520 3 0.9559518 4 0.1684311 4 0.1684317 5 0.5602483 5 0.5602498 6 0.9854805 6 0.9854798 7 0.0572346 7 0.0572373 8 0.2158350 8 0.2158448 9 0.6770011 9 0.6770234 10 0.8746824 10 0.8746509 11 0.4384523 11 0.4385469 12 0.9848475 12 0.9848941 13 0.0596914 13 0.0595110 14 0.2245135 14 0.2238778 15 0.6964287 15 0.6950261 16 0.8456631 16 0.8478592 17 0.5220680 17 0.5159758 18 0.9980520 18 0.9989791 19 0.0077768 19 0.0040795 20 0.0308654 20 0.0162512
このような性質を持つ問題は、移植と並列化によって、計算結果が変わりやすいので、問題の物理的意味を知っている方が、計算結果の検証方法を用意する必要があります。
数値的に不安定な現象を分析する手段のひとつとして、初期値にわざと乱数で誤差を加え、多数のテストケースの計算結果を統計的に分析する、「ショットガン法」があります。気象予測に使われているそうです。
配列計算の省略記法
Fortran90以降では、配列変数の計算と代入が可能です。例えば、同じ大きさの2次元配列「A, B, C」がある場合に、
c(:,:) = a(:,;) + b(;,;) * beta
のように書くと、配列の全ての要素について、計算と代入が行なわれます。この文を、
c = a + b * beta
と書いても同じ結果になるのですが、人間にとって読みにくいコードになるので、前者の書き方を推奨します。悪い例を示します。
real(8) :: a(10, 10) integer :: i, j do j = 1, 10 do i = 1, 10 a = 0.0d+0 enddo enddo
このコード断片は、一見何の問題もなく、コンパイル、実行できるのですが、ループの中で配列代入をしているので、無用な時間を消費します。
入出力装置番号
Unix系OS上のFortran処理系では、入出力装置番号0が標準エラー出力、 5が標準入力、6が標準出力に割り当てられています。これらの番号をOPEN文で指定した場合の挙動は、処理系依存です。 OPEN文の装置番号には、10から99をお勧めします。
配列計算の省略記法
Fortran90以降では、配列変数の計算と代入が可能です。例えば、同じ大きさの2次元配列「A, B, C」がある場合に、
c(:,:) = a(:,;) + b(;,;) * beta
のように書くと、配列の全ての要素について、計算と代入が行なわれます。この文を、
c = a + b * beta
と書いても同じ結果になるのですが、人間にとって読みにくいコードになるので、前者の書き方を推奨します。悪い例を示します。
real(8) :: a(10, 10) integer :: i, j do j = 1, 10 do i = 1, 10 a = 0.0d+0 enddo enddo
このコード断片は、一見何の問題もなく、コンパイル、実行できるのですが、ループの中で配列代入をしているので、無用な時間を消費します。
定義済みマクロの利用
移植可能なシステム依存のコードを書くためには、プリプロセッサーの定義済みマクロを使う方法があります。例えば、次のようなコードが可能です。
#if defined(__ia64__) integer, parameter :: ADDRESS_KIND = 8 #elif defined(__x86_64__) integer, parameter :: ADDRESS_KIND = 8 #elif defined(__i386__) integer, parameter :: ADDRESS_KIND = 4 #else #error "I do not know 32 bit or 64 bit." #endif
次のような定義済みマクロがあります。
名前 | 意味 |
---|---|
__INTEL_COMPILER |
コンパイラーがIntel製である。 |
_PGI |
コンパイラーがPGI製である。 |
__linux__ |
OSがLinuxである。 |
__SVR4 |
OSがSVR4系、たぶんSunOSである。 |
__WIN32 |
IntelコンパイラーのWindows版では 必ず(64ビットでも)定義される。 |
__WIN64 |
OSが64ビットのWindowsである。 |
__APPLE__ |
OSがMacOSである。 |
__i386__ |
CPUがx86系の32ビットモードである。 |
__ia64__ |
CPUがItanium系である。 |
__x86_64__ |
CPUがx86系の64ビットモードである。 |
H形編集
FORMAT文や文字定数のH形編は、古い機能で、使わないほうがよいです。特に、複数行にまたがったり、特殊記号を含んだりするH変換は、プリプロセッサーを誤動作させることがあります。
削除予定事項
Fortranの次の機能は、「削除予定事項」と定められ、将来のFortran規格から削除される予定です。
- 固定形式(7桁目から72桁目に文を書く伝統的な書式)
- 計算形GOTO文
- ASSIGN文および割り当て形GOTO文
- 算術IF文
- PAUSE文
- H形編集
- 文関数
- 実数型あるいは倍精度型を制御変数とするDO文
- 共有端末DO文(多重ループを同一の行番号で終わらせること)
STOP文と終了コード
「STOP」文に数値を指定すると、 Cの「exit」標準ライブラリ関数と同様に、プログラムの終了コードになりますが、標準化されていません。 UNIX系OSでは、数値を256で割った余りが終了コードになります。正常終了時には「STOP 0」または数値無しの「STOP」、異常終了時には「STOP 1」をお勧めします。
Cシェル環境では、直前に実行されたプログラムの終了コードが、シェル変数「status」に記憶されます。
次のコードは、無意味な番号を使う、悪い例です。処理系によっては、意図しないエラーメッセージが表示される可能性があります。特に、バッチ処理で問題が発生しがちです。
sample/hello> cat hello.f write(*,*) \"Hello, world.\" stop 999 end sample/hello> ifort hello.f sample/hello> ./a.out Hello, world. 999 sample/hello> echo $status 231
Intelコンパイラー利用時のまとめ
-auto -ftrapuv -check all -warn all -std -fpe0 -traceback
オプションを付けてコンパイルし、コンパイル時と実行時にエラーメッセージが表示されるかどうか確かめるようにお勧めします。','utf-8'),(13,'FortranはISOとJISで標準化され、処理系依存の部分が少ないように思われますが、ソースコードを他の処理系に移植すると、しばしば問題が発生します。本文書では、Fortranソースコードの移植性を上げるTipsを紹介します。
付録:コンパイラーオプション早見表
目的 | Intel | PGI | 富士通 |
---|---|---|---|
推奨される最適化を行なう。 |
|
-fast |
-Kfast |
配列の領域外参照を検出する。 | -CB |
-C |
-Hs |
初期化されていない変数を検出する。 | -check uninit |
|
-Hu |
全ての実行時診断機能を有効にする。 | -check all |
|
-Eg |
浮動小数点例外発生時にアボートする。 | -fpe0 |
-Ktrap=fp |
-NRtrap |
異常終了時にトレースバックを表示する。 | -traceback |
|
-Knoomitfp -A2 |
暗黙の型宣言を警告する。 | -warn declarations |
-Mdclchk |
|
標準外機能の利用を警告する。 | -std |
-Mstandard |
-v95s |
変数を静的に配置する。 | -save -zero |
-Msave |
-Knoauto |
変数を動的に配置する。 | -auto |
-Mnosave |
-Kauto |
プリプロセッサーを有効にする。 | -fpp |
-Mpreprocess |
-Cpp |
モジュールを有効にする。 | デフォルトで有効 | デフォルトで有効 | -Am |
Fortran 90の機能を使う。 | デフォルトで有効 | pgfotran コマンド |
-X9 |
REAL型を8バイトにする。 | -r8 |
-r8 |
-Ad |
デノーマル数を0に切り捨てる。 | -ftz |
-Mdaz -Mflushz |
-Kdaz |
浮動小数点演算精度の一貫性を向上する。 | -mp |
-Kieee |
-Knoeval |
自動並列化する。 | -parallel |
-Mconcur |
-Kparallel |
最も内側のループも自動並列化する。 | -Mconcur=innermost |
|
|
リダクションも並列化する。 | デフォルトで有効 | デフォルトで有効 | -Kreduction |
OpenMPを有効にする。 | -openmp |
-mp |
-KOMP |
全ての警告メッセージを有効にする。 | -warn all |
-Minform,inform |
|
x86_64環境で、2GBを超えるデータを扱う。 | -mcmodel=large |
|
デフォルトで有効 |
x86_64環境で、4GBを超えるデータを扱う。 |
|
–mcmodel=medium |
|
リスティングファイルを生成する。 |
|
|
-Qa,d,i,p,x |
付録:コンパイラーディレクティブ早見表
目的 | Intel | PGI | VAST | 富士通 |
---|---|---|---|---|
続くDOループを、自動並列化する。 | !DEC$ PARALLEL |
!PGI$L CONCUR |
!VD$ CONCUR |
|
続くDOループを、自動並列化しない。 | !DEC$ NOPARALLEL |
!PGI$L NOCONCUR |
!VD$ NOCONCUR |
|
続くDOループを、 データ依存性の疑いがあっても、 自動並列化する。 |
!DEC$ IVDEP |
!PGI$L IVDEP |
!VD$ NODEPCHK |
!OCL NOVREC |
続くDOループを、 副プログラムの呼び出しを含んでいても、 自動並列化する。 |
|
!PGI$L CNCALL |
!VD$ CNCALL |
|