この記事について
大学の研究室の後輩に向けて書いたドキュメントに加筆したものです。プログラミング超初心者の人が急に巨大なプログラム(数万行~)のデバッグをやらされることになったとき1に手引きになればと思い、公開しました。
背景となる問題意識
あまりにポエムなのでブログにまとめてます。
http://bluepost69-tech.hatenablog.com/entry/2017/11/16/191631
非情報系ではプログラミングの授業でデバッグのやり方を教えないので、問題意識を持っています。
想定している環境
- SSHでCUIしか使えない
実際の環境としては、SSH接続した先でfortran+MPIコードをデバッグするという形です。リモートで編集、実行まで行うのでIDEは使えないという状況です。スパコンでシミュレーションをやる業界なら割とよくあると思います。
大原則:プログラムの気持ちになる
まず「何が起こっているのか」「どこで止まっているのか」を調べる。
- エラーメッセージをよく読む
- デバッグオプションをつけてコンパイルしてみる
- 適当なところにwrite文を挿入して実行してみる。
- 怪しい変数はwrite文で出す
- write文の内容が出力された箇所までは計算が終わっている。
(write文は適宜言語ごとに標準出力に出力するコマンドに置き換えてください。Cだとputsやprintf、pythonだとprintなど)
特にエラーメッセージを読まない人は本当に多いです。「No such file」みたいな読めばわかるエラーを人に聞くのは恥ずべき事だと思いましょう。
Fortran runtime error: Cannot open file 'hoga.txt'
「先輩!なんか意味不明なエラーが出るんですけど!」
「cannotは中学1年生で習う英語だよ?中学校からやり直したら?」
おおまかな場所を見つけたら、プログラムの気持ちになるですよ!
- 実行する計算を追ってみる。
- 要所要所で変数をwrite文で出力する。
write文はプログラムに自身の状態を出力させる強力なツール。(ただしログが大きくなるのでデバッグが終わったら削除しておく。そのために変更箇所には目印をコメントしておくとよい。)
バグったな?と思ったら...代表的なバグとその対処法
- 計算が回らない
- コンパイルエラー
- 実行時エラー
- 計算は回るが終わらない
- デッドロック
- 無限ループ
- 計算結果が明らかにおかしい
計算が回らない
コンパイルエラー
コンパイル時にエラーが出る場合、コンパイラが直しきれない明らかな文法誤りがある。
一か所の誤りがコンパイルされないために芋づる式に誤りでない場所も指定されることがあるので、エラーメッセージをよく読んで原因を見極める。たいてい最初の方に根本原因がある。
読み切れないときは
gfortran smp.f90 > compile.log
のようにリダイレクトしたり、
gfortran smp.f90 | less
とlessにパイプして読む。
実行時エラー
原則としてコンパイルエラー同様、エラーメッセージをよく読む。ただしOSからのシグナルの場合、あまりあてにならないこともある。
SIG_SEGVが出力される場合、セグメンテーションフォールト(Segmentation fault)と呼ばれるバグに該当する。これは許可されていないメモリを参照した場合に出るエラーで、fortranの場合はほとんどが配列の引数の間違いに起因する。(1~99がallocateされた配列x(1:99)でx(100)と指定するなど)。そんな馬鹿なことはないだろうと思うかもしれませんが、変数を添え字に入れてたりすると変数が意図しない値になったりするのはよくあるんですよ。
また、
・0での割り算を実行しようとした
・平方根sqrtの中身がマイナスになった
といった場合もSIG_SEGVになることがある。
計算は回るが終わらない
デッドロックと無限ループが代表的。並列数を1にしてバグが治ればデッドロック。
デッドロック
(fortranでは)MPI特有の現象。プロセスが1対1で通信する場合、受け取る側がMPI_RECVを実行し送る側がMPI_SENDを実行しなければならないが、交互に通信する場合で両者がMPI_RECV状態になり計算が止まってしまうことがある。
対策としてはMPI_SENDRECVに書き換えるか、順番を入れ替える。これについては別記事で書きたい。
無限ループ
原則として無限ループで条件を満たした時exitするようなコードは組まない。exitが何らかの理由で実行されなかった場合や条件が満たされなかったとき、デバッグがややこしくなる。
やむを得ずwhileループを使う場合などは、上限を指定したループで条件を満たさないときにexitする形に変える。
!これはダメな例
do while(flag)
! 処理
end do
do i=1,N_LOOP_MAX
! 処理
if(.NOT. flag) exit
if(i== N_LOOP_MAX)
! まともな言語なら例外を投げるべき
write(6,*) "Error: loop not ended!"
end if
end do
計算結果が明らかにおかしい
計算過程を一つ一つ見る必要がある。
- 変数が思い通りの結果になっているか(0やNaNなど変な値が代入されていないか)
- 関数の引数がまちがっていないか(タイプミスで似た名前で宣言された別の変数が入っていることがある)
チェックしたい変数をwrite文で出力してみる。
デバッグに使うツール
grep
変数やサブルーチンがどのモジュールに書かれているのかを調べるのに使う。
例:fortran90ファイルの中で"hoge"がどのファイルに書かれているかを調べる
grep -n "hoge" *.f90
emacsの検索機能
C-sで開いているファイルで検索をかけることができる。
diff
2つのファイルを比較してどこが異なるかを出力する。作業結果をまとめるとき、バージョン間での比較に便利。
例:A.f90とB.f90の変更箇所を調べる
diff A.f90 B.f90
最後に
このレベルのデバッグをろくに教えもしないで、配属間もない学生にコード管理をやらせる研究室はもれなくブラックなので、覚悟しましょう2。