fortranのmodファイルのタイムスタンプについて

TL;DR

  • gfortranでモジュールファイルをコンパイルする時、インターフェースが変わらなければmodファイルのタイムスタンプは更新されない。
  • *.modファイルは*.oファイルと*.f90ファイルに依存する形でMakefileなどに書き込もう。
  • *.modファイルを更新すべきときは何も実行しない。

まえおき

自分の研究室には、Fortranで書かれている自家製プログラムがあるんですよ。 今どきFortranかよって思いますけど、シミュレーション界隈ではまだまだFortranが現役で使われてるところが結構あるらしいので、まぁうちの研究室もご多分に漏れずって感じなんですかね。

このプログラムはお世辞にも綺麗に書けてるとは言えず、色々文句を言いたい点があります。 そもそもFortranで書かれているっていうのは百歩譲って許しますが、Makefileの書き方が雑なんですよ。

なにが雑って、Fortranコンパイラを切り替えるためにMakefileを直接編集しなきゃいけないところとか(autotools使えよ)、 ソースコードの依存関係をちゃんと解決できていないことが原因で、コードを編集した後、再度コンパイルしようとしたらコンパイルエラーになるとか、色々です。

そこで、後者の依存関係を解決するために書き直そうとしたところ、ちょっと躓いたっていうのが主題です。

サンプルプログラム

研究室のプログラム全体をここに示すのは冗長すぎるので、サンプルプログラムを書きました。 Makefileにおけるモジュールの依存関係を書いてないところ以外は普通だと思います。

! math_const.f90
module math_const
  implicit none

  real(4) :: pi = 3.14159265359
end module
! main.f90
program main
  use math_const
  implicit none

  write(*,*) 'pi = ', pi

end program
# Makefile
FC = gfortran

.PHONY: all
all: main

main: main.o math_const.o
        $(FC) -o $@ $^

main.o: main.f90
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

.PHONY: clean
clean:
        rm -f main *.o *.mod

これと同じディレクトリでmakeすると、math_const.f90よりも先にmain.f90コンパイルしようとするので、コンパイルエラーになります。

gfortran -c -o main.o main.f90
main.f90:2:6:

   use math_const
      1
Fatal Error: Can't open module file ‘math_const.mod’ for reading at (1): No such file or directory
compilation terminated.
make: *** [Makefile:10: main.o] Error 1

これを回避するには先にmath_const.f90コンパイルしてmath_const.modを作成しておかないといけません。 この例で手っ取り早いのはMakefile中のmain.omath_const.oに関する項目の順番を入れ替えることですが、 大きなプロジェクトになるといちいちMakefile内の順番を気にしてなんていられません。 それに依存関係が関連付けられるわけでもないので、依存してるファイルが再コンパイルされない問題も起きます。

ですので、依存関係を記さなければいけません。

ダメなケース1

main.f90コンパイルしてmain.oを作成する前に、math_const.modを作成しなくてはいけないので、 main.oの依存ファイルにmath_const.modを追加しなければいけません。 ですが、math_const.modに関する項目はMakefileには無いので、サボってmath_const.oを追加してしまいましょう。

コンパイルしてmath_const.oが作成されるときには同時にmath_const.modが作成されるのでコンパイルできると思います。

# Makefile
...
main.o: main.f90 math_const.o
...

こうすると、コンパイルは通るようになります。

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

ですが、math_const.f90が更新された時にすこし不具合が生じます。

$ touch math_const.f90
$ make
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

一見すると、問題ないように見えますが、touchしただけではmath_constモジュールのインターフェースには変化がないのでmain.f90を再コンパイルする必要はないはずです。

ダメなケース2

では、諦めてmath_const.modの項目をMakefileに追加したいところですが、Makefileには一つの処理に複数の出力ファイルがある場合の対処方法が一般にはありません。 ですが、ここで思い出してください、当初の目的はmain.f90よりも先にmath_const.f90コンパイルするでした。 これを実現するだけなら、math_const.oの項目をmath_const.modに変えるだけでできますが、mainの依存ファイルにconst_math.oがあるので、 この項目を消すと厄介なことになるかも知れません。

そこで、この項目をコピペしちゃいましょう。

# Makefile
...
main.o: main.f90 math_const.mod
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

math_const.mod: math_const.f90
        $(FC) -c -o math_const.o $<
...

ここでmake clean && makeするとちゃんとコンパイルできます。

$ make clean && make
rm -f main *.o *.mod
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

math_const.f90を更新してみます。

$ touch math_const.f90
$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

main.f90が再コンパイルされていません。うまくいったように見えます。

が、実は問題があります。もう一度makeすると、

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

再度、math_const.f90コンパイルが走ってしまいます。この後何度繰り返しても再コンパイルし続けます。

modファイルのタイムスタンプ

これが何故起きるのかと言うと、gfortranでモジュール定義を含むファイルをコンパイルした時、 モジュールのインターフェースが更新されなかったら、modファイルも更新されません。 つまり、modファイルのタイムスタンプが更新されないのです。

makeなどの主要なビルドシステムはファイルのタイムスタンプで更新するべきかどうかを判断しています。 例えば、math_const.oのタイムスタンプよりもmath_const.f90のタイムスタンプの方が新しい場合、ソースコードが更新されたから再コンパイルするべきだと判断されます。

# math_const.f90の方が新しい場合
$ make math_const.o
gfortran -c -o math_const.o math_const.f90
# math_const.oの方が新しい場合
$ make math_const.o
make: 'math_const.o' is up to date.

ここで、math_const.modの例に戻ると、math_const.f90は更新されましたが、モジュールのインターフェースが変わっていません。 ですので再コンパイル時にmath_const.modのタイムスタンプは更新されず、math_const.f90のタイムスタンプよりも古いままになっています。

何度makeしてもこのタイムスタンプの新旧は変化しないため、毎度再コンパイルされる羽目になっているのです。

最終的なMakefile

そこで、この無駄な再コンパイルを阻止するためには、以下のようにします。 このようなMakefileの書き方は検索すれば出てくるやり方と大体同じです。

# Makefile
FC = gfortran

.PHONY: all
all: main

main: main.o math_const.o
        $(FC) -o $@ $^

main.o: main.f90 math_const.mod
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

math_const.mod: math_const.o

.PHONY: clean
clean:
        rm -f main *.o *.mod

math_const.modの項目は依存関係だけを記して、特に何もしません。 math_const.oを作成する時にコンパイルが走るので、そちらに依存するだけです。 こうすることで、math_const.modmath_const.oとのタイムスタンプの比較になるのですが(math_const.f90との比較では無いですが問題ないです)、 もしmath_const.modのタイムスタンプの方が古くても、ここでは何もせず、math_const.oが更新すべきかどうかの判定に移ります。 math_const.oコンパイルされるごとにタイムスタンプが更新されるので、一度コンパイルされればmath_const.f90が更新されない限り、 再コンパイルされることはなくなります。

確認すると、ちゃんと機能していることが分かります。

$ make clean && make
rm -f main *.o *.mod
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

$ touch math_const.f90

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

$ make
make: Nothing to be done for 'all'.