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

第 7 章 移植

この章では、古い Fortran プログラムを Fortran 95 に移植するときに起きる可能性がある問題について説明します。

Fortran 95 の拡張機能、および FORTRAN 77 と互換性を保つための機能については、『Fortran ユーザーズガイド』で説明しています。

7.1 キャリッジ制御

Fortran キャリッジ制御は、Fortran が最初に開発されていたときに使用されていた、機能の限られた装置から発達したものです。同じ歴史上の理由のため、UNIX オペレーティングシステムから派生したオペレーティングシステムには、Fortran のキャリッジ制御がありません。しかし、次の 2 つの方法で Fortran 95 でこの機能をシミュレートできます。


      OPEN( 6, FORM='PRINT')

このようにして開いたファイルを、lp(1) コマンドを使用して出力できます。

7.2 ファイルを扱う

Fortran の初期のシステムは名前付きファイルを使用せず、実際のファイル名と内部装置番号を対応させるコマンド行機構を提供していました。この機能は、標準の UNIX のリダイレクトなど、いくつかの方法でエミュレートできます。

例: stdinredir.data からリダイレクトします。 csh(1) を使用した例です。


demo% cat redir.data           データファイル
 9 9.9

demo% cat redir.f             ソースファイル
      read(*,*) i, z          プログラムは標準入力を読み取る
      print *, i, z
      stop
      end

demo% f95 -o redir redir.f   コンパイル
demo% redir < redir.data            リダイレクトでデータファイルを読み取り
  9 9.90000
demo%

7.3 科学技術計算用メインフレームから移植する

アプリケーションコードが本来、CRAY や CDC などの 64 ビット (または 60 ビット) メインフレーム用に開発されていた場合、UltraSPARC プラットフォームへポーティングしているときに、これらのコードを次のオプションを付けてコンパイルする場合があります。

-fast -m64 -xtypemap=real:64,double:64,integer:64

このオプションは、自動的にすべてのデフォルトの REAL 変数および定数を REAL*8 に、デフォルトの COMPLEX COMPLEX*16 に昇格させます。宣言されていない変数または単に REALCOMPLEX であると宣言されている変数だけを昇格させ、明示的に宣言された変数 (REAL*4 など) は昇格させません。単精度の REAL 定数もすべて REAL*8 に昇格されます。ターゲットプラットフォームに対して適切な -xarch-xchip を設定します。デフォルトの DOUBLE PRECISION データも REAL*16 に昇格させるには、-xtypemap の例で double:64double:128 に変更します。

詳細は、『Fortran ユーザーズガイド』または f95(1) のマニュアルページを参照してください。

7.4 データ表現

Fortran におけるデータオブジェクトのハードウェア表現に関する詳細は、『Fortran ユーザーズガイド』および『数値計算ガイド』を参照してください。通常、システムやハードウェアプラットフォーム間でデータ表現が異なると、移植性に関して重大な問題が生じます。

次のことに注意してください。

7.5 ホレリスデータ

古い Fortran アプリケーションの多くは、ホレリス ASCII データを数値データオブジェクトに格納します。1977 Fortran 規格および Fortran 95 において、CHARACTER データ型はこの目的のために提供され、その使用が推奨されています。現在でも古い Fortran のホレリス (n H) 機能を使用して変数を初期化できますが、標準的な使い方ではありません。 次の表に、データ型に適合する文字の最大数を示します。この表では、太字のデータ型は、-xtypemap= コマンド行フラグによって昇格させられるデフォルトの型を示します。

表 7–1 データ型の最大文字数

 

標準 ASCII 文字の最大文字数 

 

 

 

データ型 

デフォルト 

INTEGER:64

REAL:64

DOUBLE:128

BYTE

COMPLEX

16 

16 

COMPLEX*16

16 

16 

16 

16 

COMPLEX*32

32 

32 

32 

32 

DOUBLE COMPLEX

16 

16 

32 

32 

DOUBLE PRECISION

16 

16 

INTEGER

INTEGER*2

INTEGER*4

INTEGER*8

LOGICAL

LOGICAL*1

LOGICAL*2

LOGICAL*4

LOGICAL*8

REAL

REAL*4

REAL*8

REAL*16

16 

16 

16 

16 

例: ホレリスを使用して変数を初期化します。


demo% cat FourA8.f
      double complex x(2)
      data x /16Habcdefghijklmnop, 16Hqrstuvwxyz012345/
      write( 6, '(4A8, "!")' ) x
      end

demo% f95 -o FourA8 FourA8.f
demo% FourA8
abcdefghijklmnopqrstuvwxyz012345!
demo%

そのデータの型で使用しなければならない場合は、ホレリス定数によってデータ項目を初期化し、それをほかのルーチンへ引き渡してください。

ホレリス定数を引数として渡したり、式や比較の中で使用しようとすると、ホレリス定数は文字型の式として解釈されます。コンパイラオプション -xhasc=no を使用して、コンパイラが副プログラム呼び出しにおいて、ホレリス定数を引数の型なしデータとして扱うようにしてください。古い Fortran プログラムを移植するときにこの処理が必要な場合があります。

7.6 非標準コーディングの手順

一般的に、アプリケーションプログラムをあるシステムのコンパイラから別のシステムのコンパイラに移植するとき、非標準のコーティングを削除すれば、移植は簡単になります。あるシステムで成功した最適化や回避策が、ほかのシステムでは曖昧であり、コンパイラを混乱させることもあります。特に、特定のアーキテクチャー用に最適化された手作業によるチューニングは、ほかの場所ではパフォーマンスを低下させる原因となる可能性もあります。これについては、パフォーマンスとチューニングに関する章で説明します。しかし、次の話題は、移植に際して、一般的に考慮すべきことです。

7.6.1 初期化されない変数

局所変数や COMMON 変数を自動的にゼロに初期化するシステムもあれば、「非数値」(NaN) に初期化するシステムもあります。しかし、標準的な取り決めはありません。 したがって、プログラムは変数の初期値に関して仮定を行うべきではありません。移植性を最大限保証するために、プログラムはすべての変数を初期化すべきです。

7.6.2 別名参照と -xalias オプション

別名参照は、同じ記憶領域アドレスが複数の名前で参照されるときに発生します。通常、これは、ポインタを使用している場合や、副プログラムへの実引数が、それら実引数間で、あるいは副プログラム内の COMMON 変数間でオーバーラップしている場合に起こります。たとえば、引数 X と Z は同じ記憶領域の位置を参照します。B と H も同様です。


  COMMON /INS/B(100)
  REAL S(100), T(100)
  ...
  CALL SUB(S,T,S,B,100)
  ...
  SUBROUTINE SUB(X,Y,Z,H,N)
  REAL X(N),Y(N),Z(N),H(N)
  COMMON /INS/B(100)
  ...

古い Fortran プログラムの多くは、このような別名での参照を、当時の Fortran 言語では利用できなかった、ある種の動的なメモリー管理の手段として利用していました。

別名での参照は、すべての移植可能なコードの中で避けるべきです。一部のプラットフォーム上では、-O2 よりも高い最適化レベルを使用してコンパイルすると、予測できない結果になることがあります。

f95 コンパイラでは、規格に準拠したプログラムのコンパイルを前提としています。Fortran の規格に厳密に準拠していないプログラムをコンパイルすると、コンパイラによる解析や最適化に支障を来す状況が生じることがあります。その状況によっては、誤った結果が得られる可能性があります。

たとえば、配列の境界を越えて添字を付ける、ポインタを使用する、または、大域的変数を直接使用しているときに副プログラムの引数としても渡すといったことが行われると、コンパイラの機能が制限されて、すべての状況で正しい最適なコードを生成できなくなる可能性があります。

プログラム内に別名参照が存在することが明らかな場合は、-xalias オプションを使用して、コンパイラが考慮すべきレベルを指定してください。適切な -xalias を指定しないで、-O2 よりも高い最適化レベルでコンパイルすると、場合によってはプログラムが正しく実行されなくなることがあります。

このオプションのフラグには、別名で参照する状況の種類を示すキーワードをコンマで区切って並べたリストを指定します。各キーワードには、別名参照が存在しないことを示す no% という接頭辞を付けることができます。

表 7–2 -xalias のキーワードとその意味

-xalias のキーワード

別名で参照する状況 

dummy

副プログラムの仮引数が互いに別名で参照し合ったり、大域的な変数を別名で参照することができます。 

no%dummy

Fortran 規格に準拠しています。実際の呼び出しで、仮引数が互いを別名で参照し合ったり、大域的な変数を別名で参照することはありません。これがデフォルトです。 

craypointer

プログラムは、任意の場所を指すことのできる Cray ポインタを使用します。これがデフォルトです。 

no%craypointer

Cray ポインタは、常に特定のメモリー領域を指しているか、使用されていません。 

ftnpointer

どの Fortran 95 ポインタも、型、種類、ランクに関係なく任意のターゲット変数を指すことができます。 

no%ftnpointer

Fortran 95 ポインタは規格の規則に従っています。これがデフォルトです。 

overindex

配列の参照時に添字の境界を越えることによって発生する、配列の境界を越えた添字付けには、4 つの状況があります。 プログラムの中でこれらが発生することを許可します。 

  • COMMON ブロック内の配列の要素を参照したときに、COMMON ブロックまたは等価なグループ内の要素が参照されることがあります。

  • COMMON ブロックまたは等価なグループの要素を実引数として副プログラムに渡すと、その COMMON ブロックまたは等価なグループの任意の要素にアクセスできるようになります。

  • 連続構造型の変数が COMMON ブロックとして扱われます。そのような変数の要素は、同じ変数のほかの要素を別名で参照することができます。

  • 配列の参照がその配列内にあっても、配列の添字の各境界が越えられることがあります。

    overindex は、配列構文、WHERE 文、FORALL 文には適用されません。これらの構文で配列の境界を越えた添字付けが発生する場合は、DO ループとして構文を書き直す必要があります。

no%overindex

配列の境界を越えることはありません。配列の参照がほかの変数を参照することもありません。これがデフォルトです。 

actual

コンパイラは、副プログラムの実引数を大域的な変数として扱います。副プログラムに引数を渡すと、Cray ポインタを使用した別名参照が行われる可能性があります。 

no%actual

副プログラムに引数を渡しても、そこから別名参照が行われることはありません。これがデフォルトです。 

次に、別名参照が行われる一般的な状況の例を示します。f95 コンパイラで高い最適化レベル (-O3 以上) でコンパイルする場合は、次に示す別名参照の状況がプログラムに含まれないようにし、-xalias=no%keyword を使用してコンパイルした方が、より良いコードを生成できます。

場合によっては、生成したコードによって正しい結果が得られるようにするために、-xalias=keyword を使用してコンパイルする必要があることがあります。

7.6.2.1 仮引数や大域的な変数による別名参照

次の例の場合は、-xalias=dummy を使用してコンパイルする必要があります。


parameter (n=100)
integer a(n)
common /qq/z(n)
call sub(a,a,z,n)
...
subroutine sub(a,b,c,n)
integer a(n), b(n)
common /qq/z(n)
a(2:n) = b(1:n-1)
c(2:n) = z(1:n-1)
コンパイラは、仮変数や共通の変数がオーバーラップする可能性があることを前提と
する必要があります。

7.6.2.2 Cray ポインタによる別名参照

この例が有効なのは、-xalias=craypointer (デフォルト) を使用してコンパイルした場合だけです。


parameter (n=20)
integer a(n)
integer v1(*), v2(*)
pointer (p1,v1)
pointer (p2,v2)
p1 = loc(a)
p2 = loc(a)
a = (/ (i,i=1,n) /)
...
v1(2:n) = v2(1:n-1)
コンパイラは、これらの場所がオーバーラップしている可能性があることを前提とす
る必要があります。

次に、オーバーラップしない Cray ポインタの例を示します。この場合は、-xalias=no%craypointer を使用してコンパイルします。この方が、より良いパフォーマンスを期待できます。


parameter (n=10)
integer a(n+n)
integer v1(n), v2(n)
pointer (p1,v1)
pointer (p2,v2)
p1 = loc(a(1))
p2 = loc(a(n+1))
...
v1(:) = v2(:)
Cray ポインタは、オーバーラップしたメモリー領域を指していません。

7.6.2.3 Fortran 95 ポインタによる別名参照

次の例を、-xalias=ftnpointer を使用してコンパイルします。


parameter (n=20)
integer, pointer :: a(:)
integer, target :: t(n)
interface
  subroutine sub(a,b,n)
    integer, pointer :: a(:)
    integer, pointer :: b(:)
  end subroutine
end interface

a => t
a = (/ (i, i=1,n) /)
call sub(a,a,n)
....
end
subroutine sub(a,b,n)
 integer, pointer :: a(:)
 real, pointer :: b(:)
 integer i, mold

 forall (i=2:n)
   a(i) = transfer(b(i-1), mold)
コンパイラは、a と b がオーバーラップする可能性があることを前提とする必要があり
ます。

この例では、コンパイラは、a と b が別のデータ型のデータを指していてもオーバーラップする可能性があることを前提とする必要があります。これは、Fortran 規格に反しています。コンパイラは、このような状況を検出すると警告を出力します。

7.6.2.4 配列の境界を越えた添字付けによる別名参照

次の例を、-xalias=overindex を使用してコンパイルします。


integer a,z
common // a(100),z
z = 1
call sub(a)
print*, z
subroutine sub(x)
  integer x(10)
  x(101) = 2
コンパイラは、副プログラムの呼び出しが z への書き込みを引き起こす可能性がある
と想定する可能性があります。
-xalias=overindex を使用してコンパイルすると、プログラムは 1 ではなく 2 を
出力します。

配列の境界を越えた添字付けは古い FORTRAN 77 のプログラムの多くに含まれていますが、これらは避けるべきものです。多くの場合は、結果を予測することができません。正しい結果が得られることを確かめるには、プログラムをコンパイルし、-C (境界を越えている添字の検査) オプションを使用してテストします。これによって、配列の添字に問題があるかどうかを調べることができます。

一般に、overindex は古い FORTRAN 77 のプログラムにだけ使用することをお勧めします。-xalias=overindex は、配列、構文式、部分配列、WHERE 文、FORALL 文には適用されません。

正しいコードが生成されるように、Fortran 95 のプログラムを常に Fortran 規格の添字の規則に準拠させるようにしてください。たとえば、次の例では、配列構文式の中で不明確な添字付けが行われているため、常に、配列の境界を越えた添字付けによる誤った結果が得られます。


配列の境界を越えて添字が付けられるこの配列構文の例では、正しい結果が得られません。

   parameter (n=10)
   integer a(n),b(n)
   common /qq/a,b
   integer c(n)
   integer m, k
   a = (/ (i,i=1,n) /)
   b = a
   c(1) = 1
   c(2:n) = (/ (i,i=1,n-1) /)

   m = n
   k = n + n
C
C   a への参照は、実際には b を参照しています。
C  これは本来は b(2:n) = b(1:n-1) であるべきです。
C
   a(m+2:k) = b(1:n-1)

C  またはこれを逆に行います。
   a(k:m+2:-1) = b(n-1:1:-1)

ユーザーは直感的に、配列 b が今度は配列 c のようになることを期待しますが、実際
の結果は予測できません。

overindex フラグは配列構文式には適用されないため、xalias=overindex フラグを使用してもこの状況は修正されません。この例をコンパイルすることはできますが、生成されたコードからは正しい結果を得られません。この例を書き換えて、配列構文を等価な DO ループに置き換えると、-xalias=overindex を使用してコンパイルしたときにこのフラグが働くようになります。しかし、このようなプログラミングはそもそも避けるべきです。

7.6.2.5 実引数による別名参照

コンパイラは、局所変数がどのように使用されるかを予測して、副プログラムの呼び出しによって変更されない変数について仮説を立てます。次の例では、副プログラムで使用されているポインタが原因で、コンパイラによる最適化処理が正しく行われず、結果が予測できないものになっています。正しい結果が得られるようにするには、-xalias=actual フラグを使用してコンパイルする必要があります。


 program foo
      integer i
      call take_loc(i)
      i = 1
      print * , i
      call use_loc()
      print * , i
   end

   subroutine take_loc(i)
      integer i
      common /loc_comm/ loc_i
      loc_i = loc(i)
   end subroutine take_loc

   subroutine use_loc()
      integer vi1
      pointer (pi,vi)
      common /loc_comm/ loc_i
      pi = loc_i
      vi1 = 3
   end subroutine use_loc

take_loci のアドレスを取得して保存し、use_loc がそれを使用しています。これは、Fortran 規格に反しています。

-xalias=actual を使用してコンパイルすると、副プログラムへのすべての引数がそのコンパイル単位内の大域的な変数として見なされるべきであるとコンパイラに伝えられます。 このため、コンパイラは、実引数のように見える変数について仮説を立てるときに、より慎重になります。

Fortran 規格に反するこのようなプログラミングは避けるべきです。

7.6.2.6 -xalias のデフォルト

リストを付けないで -xalias を指定した場合は、そのプログラムが Fortran の別名参照の規則に反していないものと仮定されます。これは、別名参照の全キーワードに no% が付いていると仮定するのと同じことです。

-xalias を指定しないでコンパイルする場合のコンパイラのデフォルトは次のとおりです。

-xalias=no%dummy,craypointer,no%actual,no%overindex,no%ftnpointer

Cray ポインタを使用していても、Fortran の別名参照の規則に準拠している場合、つまり、不明確な状況でもポインタの参照によって別名参照が行われない場合は、-xalias を使用してコンパイルすることによって、より良い性能のコードが生成される可能性があります。

7.6.3 あいまいな最適化

古いコードには、古いベクトル化コンパイラに特定のアーキテクチャーに最適なコードを生成させるための、通常の計算の DO ループを再構成しているソースコードが含まれていることがあります。ほとんどの場合、この再構成は必要がないもので、しかもプログラムの移植性を下げます。よく使用される再構成は、Strip-mining (ストリップマイニング) とループの展開の 2 つです。

7.6.3.1 ストリップマイニング (strip-mining)

いくつかのアーキテクチャー上では、固定長のベクトルレジスタのために、プログラマは手作業でループ内の配列計算について、セグメントの中にストリップマイニングをしなければいけませんでした。


  REAL TX(0:63)
  ...
  DO IOUTER = 1,NX,64
     DO IINNER = 0,63
        TX(IINNER) = AX(IOUTER+IINNER) * BX(IOUTER+IINNER)/2.
        QX(IOUTER+IINNER) = TX(IINNER)**2
     END DO
  END DO

ストリップマイニングは最近のコンパイラには適切でありません。このループは、次のようにより明瞭に書くことができます。


  DO IX = 1,N
    TX = AX(I)*BX(I)/2.
    QX(I) = TX**2
  END DO

7.6.3.2 ループの展開

以前、手作業によるループの展開はソースコード最適化のための典型的なテクニックでした。しかし、現在はコンパイラがこの再構成を自動的に行います。次にループの例を示します。


  DO       K = 1, N-5, 6
     DO    J = 1, N
        DO I = 1,N
           A(I,J) = A(I,J) + B(I,K  ) * C(K  ,J)
 *                         + B(I,K+1) * C(K+1,J)
 *                         + B(I,K+2) * C(K+2,J)
 *                         + B(I,K+3) * C(K+3,J)
 *                         + B(I,K+4) * C(K+4,J)
 *                         + B(I,K+5) * C(K+5,J)
        END DO
     END DO
  END DO
  DO       KK = K,N
     DO    J =1,N
        DO I =1,N
           A(I,J) = A(I,J) + B(I,KK) * C(KK,J)
        END DO
     END DO
  END DO

前述のループは、本来意図していたとおり、次のように書き換えるべきです。


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

7.7 時間と日付関数

時刻や CPU の経過時間を戻すライブラリ関数は、システムによって異なります。

次の表に、Fortran ライブラリでサポートされる時間関数を示します。

表 7–3 Sun Fortran 時間関数

名称 

機能 

マニュアルページ 

time

1970 年 1 月 1 日からの経過秒数を返す 

time(3F)

date

日付を文字列で返す 

date(3F)

fdate

現在の時刻と日付を文字列で返す 

fdate(3F)

idate

現在の月、日、年を整数配列で返す 

idate(3F)

itime

現在の時、分、秒を整数配列で返す 

itime(3F)

ctime

time 関数の返した時間を文字列に変換する

ctime(3F)

ltime

time 関数の返した時間を現地時刻に変換する

ltime(3F)

gmtime

time 関数の返した時間をグリニッジ標準時に変換する

gmtime(3F)

etime

シングルプロセッサ:プログラムの実行で経過したユーザー時間とシステム時間を返す。 複数のプロセッサ:実測時間を返す。

etime(3F)

dtime

最後に dtime を呼び出した時点から経過したユーザー時間とシステム時間を返す

dtime(3F)

date_and_time

日付と時刻を文字と数値で返す 

date_and_time(3F)

詳細は、『Fortran ライブラリ・リファレンス』、またはそれぞれの関数のマニュアルページを参照してください。次に、これら時間関数を使用した簡単な例を示します (TestTim.f)。


      subroutine startclock
      common / myclock / mytime
      integer mytime, time
      mytime = time()
      return
      end
      function wallclock()
      integer wallclock
      common / myclock / mytime
      integer mytime, time, newtime
      newtime = time()
      wallclock = newtime–  mytime
      mytime = newtime
      return
      end
      integer wallclock, elapsed
      character*24 greeting
      real dtime, timediff, timearray(2)
c      見出しを出力
      call fdate( greeting )
      print*,    " こんにちは。現在の時刻は: , 挨拶
      print*,    " 'sleep 4' に何秒かかるか見てみよう"
      call startclock
      call system( 'sleep 4' )
      elapsed = wallclock()
      print*, "sleep 4 の実行で経過した時間: ", elapsed," 秒"
c      ここで簡単な計算に必要な CPU 時間をテスト
      timediff = dtime( timearray )
      q = 0.01
      do 30 i = 1, 100000
            q = atan( q )
30      continue
      timediff = dtime( timearray )
      print*, "atan(q) 10 万回にかかった時間: ", timediff ," 秒"
      end

このプログラムを実行すると、次のような結果になります。


demo% TimeTest
       こんにちは。現在の時刻は: Thu Feb  8 15:33:36 2001
   'sleep 4' に何秒かかるか見てみよう
   sleep 4 の実行で経過した時間: 4 秒
   atan(q) 10 万回にかかった時間: 0.01 秒
demo%

次の表に示すルーチンは、VMS Fortran のシステムルーチン idatetime との互換機能を提供します。これらのルーチンを使用するときは、f95 のコマンド行で -lV77 オプションを指定する必要があります。この場合、標準の f95 バージョンの代わりに VMS バージョンの方が使用されることになります。

表 7–4 非標準 VMS Fortran システムルーチンの要約

名称 

定義 

呼び出し手順 

引数の型 

idate

日、月、年 (d,m,y) 形式の日付 

call idate( d, m, y )

integer

time

時分秒 (hhmmss) 形式の現在時刻

call time( t )

character*8


注 –

date(3F) ルーチンおよび idate(3F) ルーチンの VMS バージョンは年を示す場合に 2 桁の値しか返さないので、2000 年問題に対応していません。これらのルーチンから返される日付を差し引いて継続時間を計算するプログラムは、1999 年 12 月 31 日以降は正しく機能しなくなります。代わりに、Fortran 95 のルーチン date_and_time(3F) を使用してください。詳細は、『Fortran ライブラリ・リファレンス』を参照してください。


7.8 トラブルシューティング

ここでは、Fortran 95 に移植したプログラムが予想どおりに動かないときに何をすればいいのかを提案します。

7.8.1 結果が近いけれども十分ではない場合

次の内容を試みてください。


      real*4 x,y
      x=99999990e+29
      y=99999996e+29
      write (*,10) x, x
 10   format('99,999,990 x 10^29 = ', e14.8, ' = ', z8)
      write(*,20) y, y
 20   format('99,999,996 x 10^29 = ', e14.8, ' = ', z8)
      end

出力は次のように表示されます。


99,999,990 x 10^29 = 0.99999993E+37 = 7CF0BDC1
99,999,996 x 10^29 = 0.99999993E+37 = 7CF0BDC1

この例では、差は 6 x 1029 です。このような大きな差異が生じる理由は、IEEE の単精度で保証されているのは 10 進 - 2 進変換に対して 10 進の 6 桁だけだからです。7 桁や 8 桁を正しく変換できる場合もありますが、これは値によって異なります。

7.8.2 警告なしにプログラムが異常終了する

警告なしにプログラムが異常終了する場合で、実行のたびに異常が発生するまでの時間が異なる場合は、次のように対処してください。