研究室の同期が Fortran のプログラムを書いていて、数日間解決できずに悩んでいたバグを修正した。LL のスクリプトばかり書いていてメモリの扱いを考慮することを忘れ、修正に時間がかかってしまったので、自戒を込めて概略をまとめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | program main integer :: var_a = 0 integer :: var_b = 0 call foo(var_a, var_b) end program subroutine foo(var_a, var_b) real(8) var_a integer var_b var_b = 123 var_a = 0 write(*, *) var_b !=> 0 end subroutine |
上記はできる限り簡略化した Fortran のコード。このコードの15行目で、標準出力に var_b
の値を表示すると、12行目で 123
を代入しているにも関わらず値が 0
となっていた。
最初サブルーチンの中身だけを追っていて、何度も問題が無いことを確認した。あるとき var_a
の値を変えると var_b
の値まで連動して変わることに気付き、ようやく何が起こっているか理解。サブルーチンを呼び出す方の、サブルーチンに渡される引数の型を見ると、サブルーチンと一致していない。メインプログラムでは var_a
は integer
で整数型、4 bytes。サブルーチンの var_a
は real(8)
で実数型の 8 bytes。
サブルーチンに変数の先頭アドレスが渡って、そのあとメモリ空間上のどこまでがその変数の長さなのかは、その変数の型によって決まる。従って長さの違う型で変数が再定義されると、先頭アドレスから間違った長さまでをその変数として用いてしまう。
概略図を以下に掲載する。右側はメモリ空間の一部を、左から 4 bytes ずつに区切ったものとして表記した。ただし簡単のため、1 byte を10進数のように書いてある。
原因が分かり、var_a
は real(8)
である必要があったため、以下のように修正。問題が解決した。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | program main real(8) :: var_a = 0 integer :: var_b = 0 call foo(var_a, var_b) end program subroutine foo(var_a, var_b) real(8) var_a integer var_b var_b = 123 var_a = 0 write(*, *) var_b !=> 123 end subroutine |
コンパイラによってメモリ空間の扱いが変わるため、このようなバグを作った場合に原因の特定が困難。サブルーチンの内外で変数の型が合致しているか要注意。
追記
研究室の先輩 @tk4terui さんに、コンパイラのオプションをちゃんと付けると、コンパイル時に警告してくれる情報を教わりました。GCC の Fortran コンパイラ ‘gfortran’ では -Wall
または -Wconversion
オプションを付けると
1 2 3 4 5 6 7 | $ gfortran -Wconversion test.f90 test.f90:13.12: var_a = 0 1 Warning: Conversion from INTEGER(4) to REAL(8) at (1) |
という感じでちゃんと警告されます。便利。
@cockscomb フォートランは参照渡しが基本なんだからずぼらこいちゃだめ!
— もっさりさんさん (@TeamMOSA2) 9月 30, 2011
@cockscomb 環境に依存するコードはソースとかに明示的に書いておかないと誰かが泣くはめになるよ!
— もっさりさんさん (@TeamMOSA2) 9月 30, 2011