本当は怖いHPC

HPC屋の趣味&実益ブログ

わたしがprintf()デバッグをする理由

ちょっと釣りっぽいタイトルを付けてしまったけれども、id:hyoshiokさんの「わたしがprintf()デバッグをしない理由」について、ちょっと前から思っていたことを書こうかと。半分ネタ、半分本気です。本気と書いてマジと読む。


まず前置きだけど、ボクは吉岡さんほど経験も技術力も知識もないし、OSのカーネルという特殊な領域のプログラミングもしたことがない、ぺーぺーの学生に毛が生えた程度のプログラマであるので、そこのところは含んでお読みいただきたい。特に、OSのカーネルという特殊性については完全に無視しているので悪しからず。

printf()デバッグは「場当たり的」で「コードがぐちゃぐちゃになる」のか

吉岡さんの記事のはてブコメントでも指摘があったけれども、まず前提としてログ用とデバッグ用の printf() は違う。ログ用の出力はきちんと構造化された手法で行うべきであって、それができていなければデバッグがどうのという以前のレベルであろう。それに対し、デバッグ用printfはソースツリーにコミットしてはいけない。

さらに、「printf()デバッグ」とは言うものの「printf」という関数名を使うのはよくない。デバッグ用のprintfは、適当にマクロで名前を置き換えて使うべきだ。とりあえずDPRINTFとでもすれば良いだろう。

#define DPRINTF printf

とする。これによって何が変わるのかというと、grepによって呼び出し箇所を探せるのだ。違うものには違う名前を付ける。そしてソースコード管理システムへのコミット前には、この関数が存在しないことをチェックする。システムに付属のbefore-commitフックの類とか、自前のラッパースクリプトを使えばよろしい。うまくやればユニットテストに含めることもできるかも。

デバッグ観」の違い

そもそも、デバッガを利用したデバッグと、printf()デバッグは、バグの原因究明という作業に対するアプローチが根本的に違う

デバッガ利用デバッグは、問題の周辺にブレークポイントを置き、周辺の変数を見たりして問題の原因を探る方法だ。変数の内容が明らかにおかしい場合や、原因がソース上で直近にある場合は発見が容易だ。しかし一方で、「一見して」間違ってはいない場合や原因が遠方にある場合は問題解決が難しくなる場合がある。つまり、デバッガ利用デバッグはパッと見て分かるかどうかに頼った「発見的な原因究明プロセス」なのだ。

それに対して、printf()デバッグは、いわば「洞察的の原因究明プロセス」である。まず、問題に対して仮説を設定し、その仮説にしたがって実験を立案し、実験を行い、その結果を考察する。そして、単純に変数の値を見ただけではわからないバグをどのように表面化させるのか、が知恵の見せ所なのである。

発見的デバッグは、仮説を立てない物理実験のようなものである。なんとなくそれっぽい実験をして、とりあえず取れるだけデータを取って、それで何かが発見できればラッキーという感じだ。

よって、発見的デバッグと洞察的デバッグには、それぞれ一長一短がある。上にも書いたように、前者は明らかにわかる単純なバグを手早く直すのに向いているのに対し、後者は「複雑・巨大なデータ構造がどこかで矛盾している」とか、「計算の中のどこかでオーバーフローしている」とか複雑な問題をじっくり解決するのに向いている。適宜使い分ければよいのである。

だが、僕はもっぱらprintf()デバッグを使う。それは、洞察的デバッグが「楽しい」からだ。
ちょっと大げさだが、複雑な現象を目に見えるように視覚化するというのは楽しいのだ。

ちょっと科学史をたどれば、先人たちが自然現象を測定するために知恵を絞って見事な実験を考えだした例はたくさんある。エラトステネスは地域によって夏至の日の太陽の南中高度が違うことを利用して地球の大きさを測定したし、フーコーは回転する鏡を使って光の速度を測定した。
この手の話を聞いたのはたぶん小学生か中学生の理科の授業だったと思うが、かなりワクワクしたのを覚えている。デバッグは、毎日がちょっとしたサイエンスなのだ

結論

みんな、もっとサイエンスしようよ!(違


P.S.
printf()を挿入するとプログラムが変わるので良くない、というのは、逆(デバッガの干渉があるとプログラムの挙動が変わる場合もある)も真なので、どっちもどっちだと思います。どっちもレアケースだと思うけど。そもそも、printfごときで動作が変わるような繊細な分野のプログラムなら、独特の手法やノウハウがあるでしょうし、それは特殊例ですね。

【広告】