Fortranは古い言語で,オブジェクト指向プログラミングには向かないが,数値計算のプログラミングには最も楽な言語なので,基本的にはFortranを使用している.常にFortranに接しているとは言え,忘れっぽいのでメモを残す.


マイ・コーディング・ルール

implicit noneを用いる

いちいち変数を宣言するのが面倒と言う人もいるが,結局はデバッグ作業の時間が短くなる. おそらく,バグの数はコードの長さのベキで増えると思われ,大規模なプログラムになればなるほどデバッグの作業時間が大半を占めるようになる.故に,変数宣言を書くことを怠って,デバッグの作業時間が増えるのはアホらしい.コーディングの時間を減らしたいのであれば,emacsなどのような高度なエディタを使って効率化が可能.

moduleの使用を推奨する

moduleを用いることで,変数名や副プログラム名の範囲をmodule内に制限することができるのが便利. 例えば,分子動力学で様々なポテンシャルのルーチンをmoduleとして書いておけば,それぞれのmodule内で同じ名前(たとえばget_rhoなど)のルーチンを定義することができる. しかし,副プログラムがあるmoduleに依存するようにプログラムを組むと,その副プログラムの移植性は低下するので,汎用性を考えると,あまりmoduleに頼りすぎない方が良いかもしれない.

subroutine内における配列の宣言は極力allocatableを用いる

f77の場合には,work配列をプログラムの先頭で宣言して,それをsubroutineに渡す必要があったが, f90以降は動的配列が許されるようになったので,その機能を使う. allocatableとせずに,subroutine内で配列を宣言するとスタック・オーバーフローで実行時エラーとなることがあり,このエラーは非常にバグ取りが難しい. ただし,頻繁にcallされるroutine内でallocate, deallocateをすると計算負荷が大きくなるので注意する.

intent文を用いる

subroutineに渡す引き数の入力・出力がすぐ分かるようになるので大事. かつ,コンパイラでエラーを検知してくれるようになる. また,subroutine呼び出し時やsubroutineからの復帰時に無駄なコピーを無くすことができるので,計算が速くなる.頻繁にcallされるsubroutineではintent(in)やintent(out)を正確に記述するべし.

配列の代入式は省略しすぎない

arr(1:n)= 1d0
arr(:)= 1d0
arr= 1d0

などは同じ作業だが,最後の表記などは一見配列には見えないので視認性が悪い.


TIPS

Intel fortranのオプション

  • -ip: inliningを行うか否か.単一のファイル内のみに適用.
  • -ipo: inliningを行うか否か.ファイル間に適用.いくつかのファイルに多くの小さな関数がある場合にはこれはquite powerfulらしい.
  • -xP, -axP: vectorization switch.-xPはすべてのループをvectorizeし,-axPはvectorizeとnon-vectorizeの2つのコード・ブランチを生成する.ループの範囲が決まっていないようなループでは有効だろう.大きなループではvectorizeした方が効率的だが,小さなループではnon-vectorizeの方が効率的.とりあえず-axPの方を選んでおけば安全? 以下の意味があるらしい. **-xW:Pentium 4 用 **-xN:Pentium 4 + SSE2 用 **-xP:Pentium 4 + SSE2 or Core Duo 用 **-xT:Core 2 Duo 用
  • -parallel: 複数コアが存在するときに,最外ループを並列化.
  • -ftz: ごく小さい値をゼロにする?
  • -tpp7: 推奨しない.-mtuneに代替されたらしい.何するか不明. 要約すると, $ ifort -O3 -ftz -ip -ipo -parallel -axP がお薦めのようだ.

GNU Fortranのオプション

ココを参照.

  • -ffpe-trap=listlistには以下があるらしい.
    • invalid (invalid floating point operation, such as sqrt(-1.0))
    • zero (divided by zero)
    • overflow (overflow in a floating point operation)
    • underflow (underflow in a floating point operation)-
    • precision (loss of precision during operation)
    • denormal (operation produced a denormal denormal value)

デバッグの際に有用なオプション

  • -fbacktrace: 何行目で止まったかを出力
  • -fbounds-check: 配列参照関連の出力
  • -fpe0: ゼロ割あんどの不正な演算の出力
  • -fcheck=all: 上記を全部? ←とりあえずこれをつけてデバッグするべし!

allocatable変数のsave

allocatable変数をsaveすることは問題ないらしい.

      subroutine sub(n)
      implicit none
      real(8),allocatable,save:: a
      logical,save:: lfirst=.true.

      if(lfirst) then
        allocate(a(n))
        lfirst=.false.
      endif

      end subroutine sub

として,サブルーチンが最初に呼ばれた時のみallocateするようにしておくこと.

**注)**これに関連して注意しなければならにことがある.*「サブルーチンの中で使用される変数は,必ず初期化して使うことを心掛けること.」*さもなくば,何度も利用されるルーチンでは,前にコールされた時の値が変数に残っていることがある.

あり得ない(と思われる)バグがあるときの対処法

例えば,以下のコード

      ......
      write(6,'(a)') " here1"
      allocate(array(n))
      write(6,'(a)') " here2"
      ......

nにはちゃんと値が入っているのに,here1からhere2へ到達しないようなことがある. このように,あり得ないと思われるバグは探すのに苦労する. こういう場合は,メモリが自分が思ったように使われていない事が多い. オプション「-check all」をつけてコンパイルしてメモリオーバーなどをチェックする必要がある.

 $ ifort -g -check noarg_temp_created -trackback

のように,noarg_temp_created という引き数を与えておいた方がよいかも.-check オプションに何も指定しないと,「an array temporary was created」という文があまりにもたくさん出てきて煩わしい.

MPI並列プログラムの場合には,オプション「-check all」をつけても,ただsegmentation faultとだけ出て,そのエラーの場所がwrite文の位置によって変化するような場合がある. これは,-check allなどのシングルスレッドに関するチェックだけでは,MPI関数でのバグを検出できないためだろう.まぁ,この場合は,MPI関数へ渡している値のどこかがおかしいということなので,そこに集中すれば良いだろう.

note: MPI関数への変数受け渡しでのバグは見つけるのが困難なので,最も気をつけるべし

Moduleの宣言に関して

例として,moda.f90が以下のようになっており,

 module moda
   implicit none
   integer:: ia
 end module moda

modb.f90が,

 odule modb
   use moda
   implicit none
   integer:: ib
 end module modb

のように,modaを参照している場合.modb.f90moda.f90の後にコンパイルされなけらばならない. また,これらを利用するtest.fが,

       program test
       use modb
       implicit none
 
       ia=1
       ib=2
       write(6,'(10i8)') ia,ib
 
       end program test

のような場合でも,modbを介してmodaの変数iaが参照される.変数だけでなくfunctionやsubroutineも同様. ゆえに,test.f内でuse modaは不要だが,書いても大丈夫なようだ.

プリプロセッサの利用

慣れると便利なのでメモしておく. Fortranの場合,拡張子を大文字にするか,コンパイラ・オプションに-cppと付けることでプリプロセッサで処理してからコンパイルしてくれる. ソースファイルtest.fに,

#ifdef TEST
      write(*,*) " I am Sam."
#else
      write(*,*) " You are Sam."
#endif

と書いておいて,

 $ ifort -cpp test.f

とコンパイルすれば,

 $ ./a.out
  You are Sam.

となるし,-DTESTを指定しておけば,以下のようになる.

 $ ifort -cpp -DTEST test.f
 $ ./a.out
  I am Sam.

静的ライブラリと共有ライブラリ(動的ライブラリ)

ライブラリとはオブジェクトファイルの集合体のようなもので,静的ライブラリは「lib???.a」という名前とし,動的ライブラリは「lib???.so」とするのが規則らしい. 静的ライブラリを作成するには,arコマンドを使用して,

 $ ar rv libxxx.a xxx.o

とすればよいらしい. これを用いてプログラムを作成する場合は,

 $ ifort -o test test.f /somewhere/libxxx.a

とする.

ライブラリに含まれるオブジェクトファイルは -fPIC オプションを付けてコンパイルされるべきのようだ.PICはPosition Independent Codeの略のようで,そのサブプログラムがいるアドレスからの相対アドレスで記述ようなプログラムコードとなる.ライブラリとして呼ばれるコードはそのように作られるべきとのこと.

静的ライブラリにモジュールが含まれている場合

モジュールは

  use some_module

のような形式でsome_module.modファイルを読み込むわけだが,これはC言語での

#include <some_module.h>

のようなものと同じで,インクルードパス内にそのファイルが存在しなければならない.

さて,静的ライブラリには一般的に,この上記の*.modファイルは含まれない. これらはsome_module.hなんかと同じで,どこかインクルードパスが通った場所にあって読み込まれなければならない. なので,/path/to/library/libsome.aに入っているsome_moduleモジュールを呼び込むソースファイルをコンパイルする際には, /path/to/include/some_module.modのありかも以下のように指定してやる必要がある.

$ gfortran -I/path/to/include -L/path/to/library -lsome hoge.f

バイナリデータの出力

open文に,

      open(ionum, file='filename', form='unformatted')
      write(ionum) a(1:n)
      close(ionum)

のようにすればバイナリで出力されるが,OSをまたぐとバイナリの扱い方が異なるので読めなくなる. 以下のようにすれば,OSをまたいでも読めるようになるかも...

      open(ionum, file='filename', form='binary')
      write(ionum) i1,a(1:n),i1
      close(ionum)

ここで,i1a(1:n)のバイナリのサイズ.a(:)がreal*8ならば,i1=n*8 byteである.読み込む場合も同様で,

      open(ionum, file='filename', form='binary')
      read(ionum) i1,a(1:n),i1
      close(ionum)

でいけるはず. 一般に,unformattedやbinaryで出力すると,ファイルサイズが下がり,出力の速度が速くなる. また,

      do i=1,n
        write(ionum) a(i)
      enddo

とやるよりも,

      write(ionum) a(1:n)

      write(ionum) (a(i),i=1,n)

とする方が高速.ファイルの出力はコンピュータの作業の中でかなり遅い作業なので,ここを高速化することはそのプログラムを高速化するのに効果的なことが多い. また,スーパーコンピュータによってはかなり高速化するものがある.

プロファイラの使用

gprofというプロファイラを使用して,実行時にどの関数がどれだけ呼ばれ,その程度の時間を要しているかなどが分かるので,高速化を行う際には必ず最初にチェックすべし. コンパイル時(とリンク時)に,「-pg」というオプションを付けてコンパイルしたプログラムを実行すると,gmon.outというファイルが生成される.

 $ gprof 実行ファイル (gmon.out)

gmon.outは付けなくても良い. こうすることで,プロファイル結果が得られる.

Getting current directory

To get current working directory in fortran, you can use getcwd() subroutine.

      PROGRAM test_getcwd
      CHARACTER(len=255) :: cwd
      CALL getcwd(cwd)
      WRITE(*,*) TRIM(cwd)
      END PROGRAM

You can use this subroutine call not only with GNU compiler but also Intel Fortran compile.

引数を取る

UnixやLinuxのコマンドの様に引数を取りたい場合,command_argument_count()関数と,getarg()サブルーチンを用いる.

      integer:: nargc
      character*128:: argv
      real*8:: x

      nargc= command_argument_count()
      do i=1,nargc
        call getarg(i,argv)
        read(argv,*) x
      enddo

などとするとargvに引数が入る.do文で回せば全ての引数を得ることができ,read文で数値に変換することも可能.

外部コマンドの利用

      call system('ls')

とかすると,「ls」コマンドが実行される. 「cd」して作業ディレクトリを変更するのはできない模様...

また,MPIで並列にsystem()関数を利用して,外部executableを実行しようとすると,

 [king01.bw.nitech.ac.jp:16967] OOB: Connection to HNP lost

のようなエラーが出て,実行できないようだ...

characterとstringの違い

参考:USING STRINGS AND CHARACTER ARRAYS

character変数の宣言方法として,

      character(len=10):: c1
      character:: c2*10
      character:: c3(10)
      c1 = '0123456789'
      c2 = '0123456789'
      c3 = '0123456789'
      write(6,'(a)') c1
      write(6,'(a)') c2
      write(6,'(a)') c3

この3つはおそらく,上二つは同じで長さ10の文字列だが, 下の c(10) は長さ1のcharacterが10個連なった配列という意味で,上二つとは異なる.

出力結果は,

0123456789
0123456789
0
0
0
0
0
0
0
0
0
0

のように,c3に関しては配列の全ての要素に0が代入されていることが分かる.

配列c3にしっかりと代入するには,

      do i=1,3
        c3(i) = c1(i:i)
      enddo

のように要素ごとに代入しなければいけない. ここで,配列であるc3にはc3(i)のように要素を指定できるが,stringであるc1にはc1(i:i)のように文字列範囲を指定しなければならない.

また,character配列を出力する際,

      write(6,'(10a)') c3
      or
      write(6,'(10a)') c3(1:10)

とすれば,

0123456789

と改行なしで出力されるようになる.


ファイル入出力

End Of File (EOF)の検出

read文に,**end=**というstatementを加えておくと,EOFを検出したら,そこで指定した行へ飛ぶことができる.

      read(10,*,end=100) c
 100  write(6,*) ' Reading file finished.'

文字列のtrim

ある文字列cに,その文字列変数の長さよりも短かい文字列を代入すると,残りの文字列は空白となる.

      character(len=5):: c
      c= 'AB'
      write(6,'(a)') c
      write(6'(a,i)') 'len(c)=',len(c)
      write(6'(a,i)') 'len_trim(c)=',len_trim(c)
      write(6,'(a,a)') 'trim(c)=',trim(c)

こうすると,

AB
len(c)=5
len_trim(c)=2
trim(c)=AB

となる.つまり,cには’AB ‘という5文字が入っており,**len(c)**で調べると文字列長は5となり,**len_trim(c)**で調べると2文字と分かる.**trim(c)**で空白を除いた文字列を取り出せる.

スラッシュを読み込む

普通に,

read(5,*) str

として読み込んだ文字列に,スラッシュ / が含まれていると,そこで文字列は終わってしまう. 例えば,../some_dirを読み込もうとしても,..のみが読み込まれたことになる.

スラッシュも読み込むためには,

read(5,'(a)') str

のように文字列をしっかりと指定して読み込む必要がある.

一行のデータ数が分からない場合

文字列データstrからdelimで仕切られたデータの数をカウントする関数num_data(str,delim)を作成し, 以下のように一行のデータ数をカウントしてから再読込する.

  character(len=128):: ctmp
  ...
  read(ionum,'(a)') ctmp
  nd = num_data(trim(ctmp),' ')
  backspace(ionum)
  read(ionum,*) (a(i),i=1,nd)
  ...

function num_data(str,delim)
  implicit none
  character(len=*),intent(in):: str
  character(len=1),intent(in):: delim
  integer:: num_data

  integer:: i

  i=1
  num_data = 0
  do
    if( i.gt.len(str) ) exit
    if( str(i:i).ne.delim ) then
      num_data = num_data + 1
      do
        i = i + 1
        if( str(i:i).eq.delim ) exit
      end do
    end if
    i = i + 1
  end do
  return

end function num_data
  • 一度,一行読み込んで,データ数をカウントした後,backspace(ionum)として一行戻っている.
  • 最初の読み込みは,read(ionum,'(a)') ctmpのように,formatをcharacterで指定して読み込まなければならない.

出力フォーマットに変数値を指定する

例えば配列arr(ndim)の長さndimが変化するような場合,その長さに応じて一行に出力する数を変更したいような場合,

      write(6,'(1000f10.2)') arr(1:ndim)

のように,十分に大きい数値(ここでは1000)のように数を指定することもできるが,

      write(cfmt,'(i10)') ndim
      write(6,'('//trim(cfmt)//'f10.2)') arr(1:ndim)

のようにして,ndimに応じた長さの出力長さを設定することができる.

ディレクトリを作る

Fortranからディレクトリを作るには,

       call system('mkdir -p dirname')

とすれば良い.この「-p」をつけないと,既にそのディレクトリが存在している場合にエラーとなってしまう.

ファイル,ディレクトリの存在を確認する

      inquire(file="path/to/file",exist=l_exists)
      if( .not. l_exists ) then
          stop ' file path/to/file does not exist !!!'
      endif

のようにして,ファイルの存在を確認できる.「file=」の代わりに「directory=」にするとディレクトリの存在を調べられると書いてるところもあるが,g95ではコンパイルエラーとなる.g95でもifortでも「file=」でディレクトリの存在も調べられる?

subroutineの引数にfunction/subroutineを!

subroutineの引数にsubroutineを渡す場合のサンプルプログラムを以下に載せておく. 重要なのは,subroutine sub2を渡すためには,program testの中で,「external:: sub2」のようにsub2をexternal指定していなければいけない. また,sub2を受け取るsub1において,interface文でsubの引数などを定義しておかねばならない. このinterface中のsubの引数の定義と,実際に渡されるsub2の引数が異なっているとエラーとなる.

program test
  implicit none
  integer:: i
  external:: sub2       !<--- external指定が必須
  i=10
  call sub1(sub2,i)

end program test
!=======================================================================
subroutine sub1(sub,m)
  implicit none
  integer,intent(inout):: m
  interface                    ! <--- interfaceでsubの定義が必須
    subroutine sub(n)
      integer,intent(inout):: n
    end subroutine sub
  end interface

  call sub2(m)
  print *,' m=',m

end subroutine sub1
!=======================================================================
subroutine sub2(n)
  implicit none
  integer,intent(inout):: n

  n=n+1
  print *,' n=',n
end subroutine sub2

LAPACKの利用

実対称行列の固有値問題(dsyev)

dsyevを用いるルーチンの例.

      subroutine eigval(ndim,dm,ev)
      implicit none
      integer,intent(in):: ndim
      real(8),intent(out):: ev(ndim)
      real(8),intent(inout):: dm(ndim,ndim)
      
      integer:: lwork,info
      real(8),allocatable:: work(:)
      
      ev(1:ndim)= 0d0
      
      lwork= 3*ndim-1
      allocate(work(lwork))
      call dsyev('N','U',ndim,dm,ndim,ev,work,lwork,info)
      if(info.ne.0) then
      write(6,'(a,i3)') " error?: info(dsyev)=",info
      stop
      endif
      
      deallocate(work)
      end subroutine eigval

ndim次元の行列dmの固有値をevに返す.

エルミート行列の固有値問題(zheev)

zheevを用いるルーチンの例.

      subroutine eigval(ndim,d,ev)
      implicit none
      integer,intent(in):: ndim
      real(8),intent(out):: ev(ndim)
      complex(8),intent(inout):: d(ndim,ndim)
      
      integer:: lwork,info
      real(8),allocatable:: rwork(:)
      complex(8),allocatable:: work(:)

      ev(1:ndim)= 0d0

      lwork= 2*ndim-1
      allocate(work(lwork),rwork(3*ndim-2))
      call zheev('N','U',ndim,d,ndim,ev,work,lwork,rwork,info)
      if(info.ne.0) then
      write(6,'(a,i3)') " error?: info(zheev)=",info
      stop
      endif

      deallocate(work,rwork)
      end subroutine eigval

ndim次元の行列dの固有値をevに返す.


Fortran90に関する覚え書き

Moduleの利用

Moduleは便利だが,コンパイルに関しては注意が必要. Moduleをuseするソースをコンパイルする際には,先にそのmoduleを含むソースがコンパイルされていなければならない. また,一つのソース内にmoduleとそれをuseするルーチンが存在する場合,moduleが上に書いてないといけない.

hogeというmoduleを含むソースhogehoge.fをコンパイルすると,中間ファイルとしてhoge.modというファイルが生成される.これは,このmoduleをuseするルーチンをコンパイルする際に必要な情報を提供する.故にhogeというmoduleに変更がなされた場合には,hogehoge.fをコンパイルしてから他のソースをコンパイルすべし.

任意長さの文字列をsubroutineで受け取る

文字列を引き数として受けとるsubroutineにおいて,文字列の長さを考慮するのはかなり面倒なことで,fortran90では(もしかしたらfortran77から?)この問題を簡単に解決することができる.

       subroutine hoge(c)
       character(len=*),intent(in):: c
 
       write(6,'(a)') " c=",trim(c)
 
       end subroutine hoge

などとすれば,渡された文字列cの空白を取り除いた(trimされた)部分だけが出力される. character(len=*)とすることで,任意の長さの文字列を受け取れるsubroutineとなる.

多次元配列の定数宣言?

どうも多次元配列で一次元配列と同じように定数として宣言しようとしたら怒られる. 次善策として,

    real(8):: hoge(1:3,1:3)
    data hoge / 1d0, 1d0, 1d0 &
                1d0, 1d0, 1d0 &
                1d0, 1d0, 1d0 /

のようにデータとして宣言した.


makefile関連

プログラマの理想

どのマシンであっても以下の2つのコマンドでコンパイルが完了することを目指す.

 $ ./configure
 $ make

マシンのアーキテクチャ依存性を吸収したmakefileを作って以上のように簡単にコンパイルが可能であれば,ユーザにプログラムを渡した際に,上記2つのコマンドだけを指示すれば良い.

autoconf

上記理想を実現するために,アーキテクチャ依存性を吸収する方法がautoconfによって与えられている. autoconfを実行すると,configure.acとMakefile.inを利用してconfigureスクリプトが生成される. このconfigureを実行することで,マシン依存のMakefileが生成される. 故に,プログラマがユーザに渡すべきものは,ソースコード一式とautoconfによって生成されたconfigureとMakefile.inである.

実際にFortranプログラムにautoconfを導入した記録

autoscan

まずは,ソースの存在するディレクトリ上で,autoscanを実行とすると,いくつかのエラー文が出力されるがconfigure.scanというファイルが生成される.configure.scanの中身はこんな感じ.

 #                                               -*- Autoconf -*-
 # Process this file with autoconf to produce a configure script.
 
 AC_PREREQ(2.59)
 AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
 AC_CONFIG_SRCDIR([params_eam_al.h])
 AC_CONFIG_HEADER([config.h])
 
 # Checks for programs.
 AC_PROG_CC
 AC_PROG_CPP
 
 # Checks for libraries.
 # Checks for header files.
 # Checks for typedefs, structures, and compiler characteristics.
 # Checks for library functions.
 
 AC_CONFIG_FILES([makefile])
 AC_OUTPUT

configure.ac

そのconfigure.scanファイルをconfigure.acと名前変更し,configure.acファイルを編集する.

 #                                               -*- Autoconf -*-
 # Process this file with autoconf to produce a configure script.
 
 AC_PREREQ(2.59)
 AC_INIT([adaptive-MDCG], [1.0.0], [[email protected]])
 AC_CONFIG_SRCDIR([pp_main.f])
 #AC_CONFIG_HEADER([config.h])
 
 # Checks for programs.
 AC_PROG_F77([mpif90])
 AC_PROG_CPP
 
 # Checks for libraries.
 # Checks for header files.
 # Checks for typedefs, structures, and compiler characteristics.
 # Checks for library functions.
 
 AC_CONFIG_FILES([makefile])
 AC_OUTPUT
  • Fortranを使用するので,AC_PROG_F77というマクロを書いている(mpif90を指定しているが…).
  • AC_CONFIG_SRCDIR()には,そのディレクトリ内にあるソースファイルを一つだけ指定(ディレクトリを特定できるものであればよい).
  • AC_CONFIG_FILES()には最終的に出力されるファイル名を記述.
  • 階層構造がある場合には,AC_CONFIG_FILESに,各階層のmakefileを記述し,各階層のディレクトリにmakefile.inを作成しなければならない.例えば,
  AC_CONFIG_FILES([makefile src/makefile])

makefile.in

makefile.inというファイルを用意する.(automakeというコマンドでmakefile.amからmakefile.inを作成することが出来るが,高機能故に可読性が悪いので,ここでは使用しない.)

 #!/usr/sh
 
 # cpp path
 CPP = @CPP@
 #-----------------------------------------------------------------------
 # Automatically generated by configure
 MPIFC= @F77@
 MPIFLAGS= @FFLAGS@
 FC= $(MPIFC)
 FFLAGS= $(MPIFLAGS)
 #-----use MPIFC even for serial codes
 .SUFFIXES: .o .f .f90
 .f.o: 
       $(MPIFC) -c $(MPIFLAGS) $<
 .f90.o:
       $(MPIFC) -c $(MPIFLAGS) $<
 #-----------------------------------------------------------------------
 
 force= force_eam_al.o force_lj_ar.o
 mdcg= main.o md.o cg.o md_conv.o lasubs.o sort.o util_tag.o $(force)
 
 all: mdcg 
 
 mdcg: $(mdcg)
       $(MPIFC) $(MPIFLAGS) -o $@ $(mdcg)

こんな感じで,@F77@のようにマシン依存するようなところ(configureによって決定してもらいたいところ)に@で挟んだキーワードを書いておく.どんなキーワードが存在するのかは良く知らない.

autoconf

 $ autoconf

としてautoconfを実行すると,configure.acmakefile.inからconfigureスクリプトが生成される.

コンパイル

 $ ./configure
 $ make

でコンパイルできるようになる.

プリプロセッサに関して

大抵のFortranコンパイラの場合,拡張子が**.F.F90のように大文字になっていれば,プリプロセッサを通してからコンパイルする.なので,makefileに以下のように書いておくだけで,.fだろうが.F**だろうが関係なくコンパイルしてくれる.

 .SUFFIXES: .o .f .f90 .F .F90
 .f.o:
      $(FC) -c $(FFLAGS) $<
 .f90.o:
      $(FC) -c $(FFLAGS) $<
 .F.o:
      $(FC) -c $(FFLAGS) $<
 .F90.o:
      $(FC) -c $(FFLAGS) $<

けれども,どうも**.mod**ファイルが存在するとmakeの挙動がおかしくなる.例えば,

 m2c -o hoge.o hoge.mod

のようなコマンドを勝手に実行しようとしてしまい,

 m2c    -o hoge.o hoge.mod
 make: m2c: No such file or directory
 make: *** [hoge.o] Error 1

のようなエラーで止まってしまう.この問題を解決するには,

 %.o : %.mod

という行をmakefileに加えれば良いらしい.


ソースコード解析

大きなプログラムのソースコードを読まなければならないこともある.もともと人が書いたソースコードを読むことが辛い作業なのに,それが大量にあると考えると,頭が痛くなる... そこで,そのプログラムの大局的な構造がどうなっているかだけでも知れれば作業がだいぶ楽になるだろう.

doxygen

doxygenは上記の要望をかなえるソフトウェア.

  • doxygen: generate documentation from source code

基本的には,

$ doxygen <conf-file>

のように <conf-file> を代入して,そのディレクトリ以下にあるソースコード群の解析を行う. <conf-file> は以下のようにして基本的なものを作成することができる.

$ doxygen -g <conf-file>

この中の PROJECT_NAME くらいは変更して,コード解析するものの名前にしておいた方がよい.


デバッグ

Ref.9 に詳しいが,プリプロセッサ(CPP)を用いたデバッグテクニックを覚えておくと便利なようだ. 拡張子が.F90のように大文字となっていると,FortranコンパイラがそのソースコードはCPPで処理されるものと判断し,CPPで処理してからコンパイルする.もしくは,-CPPオプションをつけることでCPP処理を強制することもできる. makefileでコンパイルする場合には,おそらくCPPFLAGSという変数がコンパイルコマンドでオプションとして渡されていると思うので,

$ make CPPFLAGS=-D_DEBUG <program-name>

のようにして_DEBUGを指定することができる.

_DEBUGの指定

#ifdef _DEBUG
    デバッグ作業コード
#endif

上記のように,#ifdef _DEBUGから#endifまでが,_DEBUGが指定されたときにのみCPPを通過してコンパイルされるコードに残る.

下記のように,デバッグとは別のコードとすることも可能.

#ifdef _DEBUG
    デバッグ作業コード
#else
    別のコード
#endif

DEBUG_PRINT関数の定義

以下のように,_DEBUGを指定されたときのみ与えられた変数を出力する関数DEBUG_PRINTを定義する.

#ifdef _DEBUG
#define DEBUG_PRINT(a) write(*,*) (a)
#else
#define DEBUG_PRINT(a)
#endif

これを用いて調べたい変数hoge

  DEBUG_PRINT(hoge)

とすることで_DEBUGが指定されたときににのみ変数を出力するようになる.

コンパイラオプション

  • -O4などのような最適化オプションをつけるとデバッグのエラーが分かりにくくなることがあるので,-O0として最適化レベルを下げてコンパイラが正確な情報を出してくれるようにする.
  • -fcheck=boundsとして,メモリ範囲の外側にアクセスしようとするようなエラーをちゃんとチェックするようにする.

Reference

  1. USER NOTES ON FORTRAN PROGRAMMING (UNFP)
  2. Fortran 90 骨の髄まで 12-6 コンパイラの名前とautoconf
  3. autoconfの使用方法- hippos-lab::net
  4. autoconf --- Fortranコンパイラの特徴
  5. Autoconf:
  6. 本物のプログラマーはpascalを使わない
  7. Fortranを使おう
  8. Computer and network
  9. Fortran演習 (地球惑星物理学演習@東大理学部)