Fortran プログラムで変数の型に関するバグ

研究室の同期が 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_ainteger で整数型、4 bytes。サブルーチンの var_areal(8) で実数型の 8 bytes。

サブルーチンに変数の先頭アドレスが渡って、そのあとメモリ空間上のどこまでがその変数の長さなのかは、その変数の型によって決まる。従って長さの違う型で変数が再定義されると、先頭アドレスから間違った長さまでをその変数として用いてしまう。

概略図を以下に掲載する。右側はメモリ空間の一部を、左から 4 bytes ずつに区切ったものとして表記した。ただし簡単のため、1 byte を10進数のように書いてある。

原因が分かり、var_areal(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)

という感じでちゃんと警告されます。便利。