Yifeitao | 次生進化

「デバッグの法則」(Debugging Rules!)——デバッグは技術である

⚠️ この記事は古くなっています:最終更新は 2016-12-24(9 年以上前)です。内容が古い可能性がありますのでご注意ください。

一般に、エンジニアがあるものを芸術、あるいは玄学と呼ぶときは、その難度が高く、従うべき規則があまりないことを意味する。デバッグはこの範疇に属する。ほとんどすべてのプログラマは、バグに翻弄されて七転八倒した経験を持つ。時にあれこれいじっているうちに問題は解決したように見えるが、なぜ解決したのかは分からない。それを説明不能な玄学現象として扱うしかない。最も厄介なバグは、たまにしか現れず再現が難しいもので、学名をハイゼンベルクバグという。

しかし、ソフトウェアはやはり科学技術の範疇に属するものであり、デバッグもまた技術的な仕事であるべきだ。「デバッグの法則」(Debugging Rules!)は、デバッグ技術を扱った珍しい書物である。九つの普遍的な原則は、ソフトウェアとハードウェアの開発・保守に当てはまるだけでなく、日常生活にさえ応用できる。この本を見つけるのにも少し苦労したが、幸いなことに図霊コミュニティには正規の電子書籍 が売られていた。

正直に言えば、著者が挙げる例の多くはハードウェア寄りで、理解するのにやや隔たりを感じる。しかし九つの原則は実に実用的で、私の以前の開発・デバッグの経験の中でも無意識にいくつかはまとめていたかもしれない。それでもこの本を読むと、武術の心法を手に入れたような感覚がある。読んだ後、以下で九つの原則をもう一度たどり、理解を深める。

1 システムを理解する

これは最も重要な原則である。バグを分析するには、当然システムがどのように動作するかを理解する必要がある。そのためには基本原理をいくつか学び、身につける必要があり、具体的なクラスライブラリ、ツール、技術については関連するドキュメントを真剣に読む必要がある。私が公式ドキュメントを真剣に読む習慣を身につけたのはここ数年のことである。検索エンジンの発達によって、我々は google 指向プログラミングの習慣を身につけてしまった。問題に直面すると、いつも一気に解決しようとする。結果として多くの回り道をすることになる。道標を見る時間を惜しむからだ。本書は特に「一字一句」マニュアルを読むことを強調しており、これは浮ついた人間への頭からの一撃である。システムを理解せずにデバッグを始めることと、原理を理解せずにコードを書くことは一脈相通じるもので、俗に「偶然指向プログラミング」と呼ばれる。

2 失敗を再現する

この原則はバグを再現することの重要性と方法を説く。バグは必ずある特定の条件下で発生する。その特定の条件を丹念に突き止めれば、半分は成功したも同然である。私のハードウェア保守の経験から言えば、バグを発見した人がその過程全体を詳細に記録できれば、非常に役立つ。多くのバグは発生条件が厳しく、だからプログラマの口癖の一つが「私のところでは問題ないです」なのである。こうしたバグに対しては、まずシミュレーションの方法を見つけるのがよい。たとえばソフトウェアの動作や相互作用の速度を上げる、ストレステストを行うなどである。次に、ソフトウェア内部に詳細で合理的なバグ記録の仕組みを持ち、内部から再現条件を見つけやすくする。

3 考えず、見る

この原則は観測の重要性を強調する。空想するよりも現実に向き合うことが大切である。言語は思考に影響する。だからバグに遭遇したときの口癖は「ちょっと見てみます」であるべきで、「たぶん〜が原因だと思います」ではない。我々はあらゆる手段を尽くしてバグの細部をはっきりと見るべきであり、成熟したプログラムにはデバッグ用の基盤が備わっているべきである。私の経験から言えば、少なくともログは不可欠である。初級のプログラマはブレークポイントだけに頼ってデバッグする習慣があるが、問題はブレークポイント自体がプログラムの実行フローを変えてしまうことである(特にマルチスレッドの場合)。もちろん推測も依然として有用で、観測の範囲を絞り込む助けになる。あるいは少なくとも観測の優先順位を決めるのに役立ち、そこで経験が生きる。しかしどうであれ、経験は観測に代わりえない。

4 分割統治

二分探索法は探索の時間計算量を線形から対数に変えられる。これはプログラム中の探索アルゴリズムだけでなく、デバッグ時の方法論でもある。システムにこの意識があれば、鍵となるのはシステムをどう分割するかであり、ここで原則1に戻る。少なくともブレークポイントでデバッグするとき、バグの位置を見つけるのに二分法が使えるし、ログを挿入するときにもこの原則を応用できる。さらに本書ではバグ同士が互いに影響し合う現象にも触れている。私はむしろバグのシーソー現象と言ってもよいとさえ思う。解決の道は一つも見逃さないことである。バグをデバッグしているうちにコードの品質があまりに悪いことに気づき、リファクタリングが必要になることもある。そうでなければ砂の上に高台は築けない。効果的なリファクタリングを経て、バグが自然に発見され解消されることさえある。

5 一度に一箇所だけ変える

この原則は実験科学における対照の原則に似ている。一度に一つの変数だけを考察し、正常な状況と異常な状況を比較し、いかなるテスト条件の差異も見落としてはいけない。前の条ではバグを一つも見逃さないと言ったが、一つずつ片付ける必要もある。リファクタリングの際も、理解していないコードをやたらにいじらないよう注意すべきである。多くのコードの危険性は、一箇所を触ると全体に響くことにある。これがバグ修正時の最大のリスクだと思う。しばしば一つのバグを消すと同時にいくつかのバグを増やすことになる。やはり原則1に戻り、システムを理解することである。とはいえ、先人が書いたコードを理解するのは容易ではないこともある。これはまた、コードを書くときに徳を積むべきことを我々に示している。コードはコンパイラのためにあるのではなく、後に我々のコードを保守する人のためにあるのだ。

6 監査証跡を残す

この原則は記録の重要性と方法を説く。医者が診察するのと同じで、適切な質問をし、具体的で有効な答えを出す必要がある。私の経験では、バグのデバッグを始めると、人はすっかり深みにはまって抜け出せなくなることがある。記録を取ることで、思考の泥沼から自分を引き離し、デバッグの計画と方法を絶えず修正できる。専用のシステムソフトウェアでは、ソフトウェアの利用者に、バグの発生状況を有効な言葉で記録する方法を訓練する必要がある。記録がなければ、デバッグする側は適切な質問で利用者の記憶を呼び覚まさねばならない。また、深夜に解決できないバグは一眠りする必要があると思う。拡散思考が自動的に働き、たいてい朝には解決している。

7 プラグを確認する

この原則は、いわゆる「初歩的なミス」について述べている。母が「パソコンのスピーカーから音が出ない」と言ってきたら、まず電源が入っているかを聞く、という類である。あるいは main 関数を mian 関数と書いてしまうとか。一度、ブレークポイントを置いてデバッグしてもブレークポイントで止まらず、後で分かったのは、似たコードが二箇所あり、ブレークポイントを間違った場所に置いていたことだった。シナリオと経験に照らして、そのシナリオでよくある初歩的なミスを犯していないか自問すれば、多くの場合すぐに効果が出る。多くの電気機器の取扱説明書の故障 FAQ でも、プラグの確認が強調されている。初歩的なミスが「初歩的」なのは難度であって、頻度ではない。

8 新しい視点を得る

この原則は助けを求めることと交流について述べている。ここで重要なのは、他者の視点によって自分の思考の固定観念を打ち破ることである。だから相手には現象だけを詳細に説明し、自分の推測は言わないほうがよい。相手の判断を汚染してしまうからだ。プログラマにとって、このときの google 指向プログラミングの力は驚異的である。おそらく九割の問題は、実際に質問する必要はなく、適切な検索語を見つけて検索すればよい。中国のプログラマにとって、英語で検索することは必須の技能である。もちろん、見つけたものを本当に理解することも非常に重要で、薬を見たらすぐ飲む、ではいけない。もし google で本当に検索できなければ、あなたのレベルが少し高い証拠である。そのときは stackoverflow などのコミュニティで質問すればよい。

9 バグを修正しなければ、バグは依然として存在する

この原則は、甘い期待を抱くな、バグが一瞬顔を出しただけだからといってダチョウ政策を取るな、ということを述べている。来るべきものは必ず来る。報いがないのではない、時が来ていないだけである。もちろん実際の状況は複雑でありうる。ソフトウェア工学はもともと時間・コスト・品質の妥協の産物である。しかし、たとえ一部のバグを見逃すとしても、それによって生じうる結果を分析し、十分な防御機構を築くべきである。だから多くのソフトウェア認証体系にはソフトウェア安全水準の規格がある。ここにはバグの再現と一を聞いて十を知るという問題も関わる。本当にバグの原因を見つけ、有効な修正手段を提供したことを確認し、同様の問題をすべて洗い出す必要がある。たとえばメモリリークを一つ修正したら、そうしたリークを引き起こすすべてのコードを修正しなければならない。

附記:デバッグしやすいコードの書き方

デバッグの原則から逆に見ると、ソフトウェアの構築自体にも大いに役立つ。何しろデバッグは穴を塞ぐだけのものであり、書かれるコードのバグが少ないほうがよく、デバッグのときに便利なように手段を多く用意しておくのがよい。ここでも個人の経験を簡単にまとめておく。不断の改善が必要かもしれない。

  1. プログラムにはログの仕組みを備えるべきで、プログラムの debug 版ではデバッグ情報を出力しやすくする。release 版であっても、ソフトウェアに問題が発生したとき(たとえば C# で例外が投げられたとき)の詳細な状況を記録すべきである;
  2. 人間が読むのに適したコードを書くべきである。そうでなければ後の保守をする人が読んでも分からない。どうデバッグせよというのか。どう書くかは『コードコンプリート』に最も詳しく書かれている。各種の慣習的な書き方のほかに、相手の身になって考えることも重要だと思う;
  3. 各種のクラスライブラリやツールを使うときは、できるだけ原理を理解し、公式ドキュメントを丹念に読む。たとえばメモリリークをどう防ぐか、マルチスレッドの状況にどう対処するかを詳しく説明しているクラスライブラリもある。よく見てから使えば、禍根を植えることが少なくなる。少なくともデバッグのときに蠅のように当てもなく原因を推測する必要はなくなる;
  4. テストしやすいコードを書く。そうすればデバッグのときに問題を切り離しやすく、二分法も使いやすい。この点は TDD の実践が比較的十分に論じており、私自身も強化する必要がある。