Fortranの移植性

提供: floatingexception
移動: 案内検索

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を紹介します。

付録:コンパイラーオプション早見表

目的IntelPGI富士通
推奨される最適化を行なう。 -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
-i-dynamic
デフォルトで有効
x86_64環境で、4GBを超えるデータを扱う。 –mcmodel=medium
リスティングファイルを生成する。 -Qa,d,i,p,x

付録:コンパイラーディレクティブ早見表

目的IntelPGIVAST 富士通
続く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