Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

130613-debug

16,758 views

Published on

デバッグのコツ、特にsort+diffでバッグの例とバージョン管理システムを使ったデバッグの例の紹介。

Published in: Technology
  • Be the first to comment

130613-debug

  1. 1. 1/33 デバッグのコツ 渡辺宙志2013年6月13日 ※これは2013年6月6日に行われたCMSI計算科学技術特論Aの内容を一部抜粋、改訂したものです
  2. 2. 2/33 「デバッグ」という作業バグを見つけたら、デバッグするまで先に進めなくなる → デバッグは絶対にやらなければいけないタスク デバッグは・・・ 集中力を要求する 成功したら達成感がある 仕事した気になる → 実際には自分で入れたバグを自分で取っただけ(マッチポンプ) 「デバッグは仕事ではない」ということを肝に銘じること ※他人の入れたバグを取るのが仕事の人は愁傷様です
  3. 3. 3/33 典型的な研究スパン年に二編論文を書きたい→ 半年で一つの研究が完結プログラム開発+計算 執筆 調査 調査:先行研究の調査や、計算手法についての調査 (1ヶ月)開発+計算:プログラム開発、計算の実行(4ヶ月)執筆:結果の解析+論文執筆+投稿 (1ヶ月)実態は・・・ 執筆 調査 デバッグ 開発 開発時間の大部分はデバッグに費やされている初心者であるほど、デバッグの占める割合が長くなるコードの高速化は、研究時間の短縮にさほど寄与しない 計算 ※ 一般論です
  4. 4. 4/33 Q. 最適化、並列化でもっとも大事なことは何か?A. バグを入れないこと 開発において最も時間のかかるプロセスはデバッグ→ バグを入れない事が最も効果的な高速化デバッグについてそもそもバグを入れないコーディング&バグってもすぐに問題個所を発見できる状況の構築を目指す 並列プログラムのデバッグは絶望的に難しい・デバッグ用のprint文入れたら挙動が変わる・ほとんど動くがたまにこける・実行環境によってはこける, etc.
  5. 5. 5/33 バグの入り方と種類Q. バグはいつ入るか?バグの種類:・機能追加直後に判明するバグ(即効性バグ) → バグを入れないコーディング・機能追加後、後で判明するバグ(地雷型バグ) → 地雷型バグのデバッグA. 機能を追加したとき※ より正確には、仕様を変更した時
  6. 6. 6/33 バグを入れないコーディング 単体テストとsort+diffデバッグ
  7. 7. 7/33 いろいろあるが、特に以下の二つの方法が有効(一種のテスト駆動開発)バグを入れないコーディング・単体テスト・sort + diff デバッグ ※プログラム開発一般論としては、将来の仕様変更に強い  設計をすることの方が大事ですがここでは触れません
  8. 8. 8/33 単体テスト・テストしようとしている部分だけを切り出す → 該当部分のみでコンパイル、動作テストできる   最低限のインタフェースを用意する・最適化や並列化の前後で結果が一致するか確認・本番環境でテストしない
  9. 9. 9/33 sort+diffデバッグ・出力情報を保存し、sortしてからdiffを取る→ 二種類の方法による結果が一致することを確認・print文デバッグの一種・単体テストと組み合わせて使う ※なんだかんだいってprint文でバッグがデバッグの基本
  10. 10. 10/33 デバッグのコツ 「ここまでは大丈夫」という砦を築く
  11. 11. 11/33 ペアリストとは? ・粒子間距離が相互作用距離未満である粒子対のリスト ・全粒子対についてチェックするナイーブな実装→O(N^2)    グリッド探索 sort+diff デバッグの例1:粒子対リスト作成 (1/2) ・空間をグリッドに切り、粒子の住所録を作成・相互作用する粒子は空間的に近いところにいるはず・住所録から粒子番号を逆引きして相互作用粒子を探索→O(N)この範囲だけ探索
  12. 12. 12/33 ポイント O(N)法とO(N^2)法は、同じconfigurationから同じペアリストを作るO(N^2)法は、計算時間はかかるが信頼できる (砦)手順 初期条件作成とペアリスト作成ルーチンの切り出し(単体テスト)O(N)とO(N^2)ルーチンに同じ初期条件を与え、ペアリストをダンプダンプ方法:粒子対の番号が若い方を左に、一行に1ペアリストとしては一致するはずだが、リストの順番は異なる→ ソートしてからdiffを取る$ ./on2code | sort > o2.dat$ ./on1code | sort > o1.dat$ diff o1.dat o2.datいきなり本番環境に組み込んで時間発展、などとは絶対にしない ←結果が正しければ何も出力されない sort+diff デバッグの例1:粒子対リスト作成 (2/2)
  13. 13. 13/33 端の粒子の送り方 ナイーブな送り方 通信方法を減らした送り方 隣接するドメイン全てと通信を行う3次元の場合、26回の通信が発生する Domain A Domain B Domain C 辺で接する領域からもらった粒子を別の方向で辺で接する領域へ転送 斜め方向の通信が必要なくなるため通信回数は6回で済む sort+diff デバッグの例2:粒子情報送信(1/2)
  14. 14. 14/33 (1) 初期条件作成と通信ルーチンのみで実行  (単体テストの原則)(2) 通信後、自分の担当する粒子を全て出力   (proc012.datなどの名前でファイルに出力する)(3) ナイーブな通信(砦)と、転送式の通信の両方で実行  (出力先を test1/ test2/などと異なるディレクトリに)(4) 粒子の座標が完全に一致することを確認 (sort + diff デバッグ)デバッグの手順 自分の領域 受け取った領域 全てのプロセスについて一致することを確認$ sort test1/proc000.dat > test1/proc000s.dat$ sort test2/proc000.dat > test2/proc000s.dat$ diff test1/proc000s.dat test2/proc000s.dat$ diff test1/proc001s.dat test2/proc001s.dat…sort+diff デバッグの例2:粒子情報送信(2/2) ※複数の初期条件について試すこと
  15. 15. 15/33 ペアリストの並列化 並列版空間分割による並列化各領域でそれぞれペアリストを作成 並列化の有無に関わらず同じconfigurationからは同じペアリストを作成しなければならないsort+diff デバッグの例3:並列版リスト作成(1/2) 非並列版
  16. 16. 16/33 手順 非並列版と並列版のペアリスト作成ルーチンを作るそれぞれに同じ初期条件を与える非並列版は標準出力にダンプ並列版はプロセスごとにファイル(proc???.dat)に出力→ あとでcatでまとめるsort + diffで一致を確認するポイント 非並列版のペアリスト作成ルーチンはデバッグが終了 (砦)粒子情報の通信ルーチンはデバッグが終了 (砦)一度に複数の項目を同時にテストしない sort+diff デバッグの例3:並列版リスト作成(2/2) $ ./serial | sort > serial.dat$ ./parallel$ cat proc???.dat | sort > parallel.dat$ diff serial.dat parallel.dat
  17. 17. 17/33 新しい機能の追加や高速化をするたびに単体テストする単体テストとは、ミクロな情報がすべて一致するのを確認することエネルギー保存など、マクロ量のチェックは単体テストではない時間はかかるが信用できる方法と比較する複数の機能を一度にテストしない デバッグとは、入れたバグを取ることではなくそもそもバグを入れないことであるバグを入れないコーディングのまとめ単体テストとは、必要なルーチンのみでコンパイル、実行すること全体のプログラムの一部に着目してテストすることではない「確実にここまでは大丈夫」という「砦」
  18. 18. 18/33 地雷除去 地雷型バグのデバッグ方法
  19. 19. 19/33 デバッグ・・・その前にバージョン管理システム、使っていますか?(Y/y) バージョン管理システムとは ファイルの編集履歴を管理するためのシステムCVS, Subversion, Gitなどが有名編集履歴を全て記録する「リポジトリ」というデータベースをもつユーザは、そのリポジトリにアクセスしながら開発を行う超優秀な秘書のようなもの リポジトリ checkoutupdate commitcommitcheckoutupdate
  20. 20. 20/33 コード 1)開発したコードをスパコンへ コード ローカル スパコン ありがちなパターンコードB 3)スパコンで実行中、別の修正をする コードA 2)動かなかったので苦労して修正する コードB 4)修正したコードをスパコンへ あっ、コードAを上書きしちゃった!
  21. 21. 21/33 バージョン管理している場合ローカル スパコン リポジトリ コード 1)開発したコードをリポジトリへ コード コード 2)リポジトリからスパコンへチェックアウト コードA 3)動かなかったので苦労して修正する コードA 4)修正をコミット コードB 5)スパコンの修正を忘れて別の修正 衝突 6)修正をコミットしようとして、衝突に気づく コードC 7)スパコン向けの修正と新しい修正を統合 (マージ)
  22. 22. 22/33 バージョン管理システムの利点・全ての編集履歴が保存される (ちゃんとコミットしていれば)・(リポジトリを別マシンに置けば)バックアップの代わりにもなる → 個人ユースでは地味に便利 ・複数の環境でコードを開発しても混乱が少ない → むしろ複数の環境向けにコードを開発する時には   バージョン管理必須 ・任意のバージョン間の比較が可能  → どのようにデバッグに役に立つかは後述
  23. 23. 23/33 バージョン管理システムの欠点 (面倒な点)・修正前に最新の状態にアップデートしなければならない  → 慣れると習慣になります ・衝突(コンフリクト)が発生した時に対処しなければならない。  → 衝突に気づかずに上書きしてしまうほうが怖いです ・全ての修正を「コミット」しなければならない  → 慣れると習慣になります ・すべての履歴を保存って、ディスク容量を食うんじゃないの?  → 差分を保存していくので、たいしてディスク容量は増えません → そもそもテキストファイルの容量なんて無視できるレベル
  24. 24. 24/33 地雷型バグ地雷型バグとは? バグを入れた後、しばらくしてから発見されるバグ・最初から入っていたが、これまで気づかなかった・機能追加時に、思わぬところに影響が波及した、etc バグを見つけたら? ・いきなりデバッグをはじめないデバッグにおいて重要なのは原因究明「いつのまにかなおっていた」は一番まずい→ 最初にやることは現場保全(1) 再現性テスト (同じ条件で実行したら同じバグを発生するか?)(2) バグを起こすソース一式を保存しておく (Subversionならタグ)(3) バグを再現する最低限のコードを切り出す (容疑者の限定)A B C
  25. 25. 25/33 バグったコードの保存バグったコードは保存しておく Subversionを使っているなら、tagという機能を使うとよい trunk tags ソース一式 130613_bug ソース一式 ジョブスクリプト Subversionにおいてタグとは、単にコピーのことデバッグが終了したら消しても良い (消去したことも含めて記録される) なぜ保存しておくか? デバッグしたつもりが、実はなおってなかったということがよくある(別の原因でバグが発生しなくなったのを完治したと勘違い)後で同様なバグが発生した時、同じ原因か、別のバグなのかを確認したいことがよくあるため
  26. 26. 26/33 問題の切り分け (1/3)実行したらSegmentation Faultと言われて止まったやってはならないこと・どこで止まったかを調べる・どうやって調べるか? → print文による二分探索 (gdbでも可)→ いきなりソースを見ながら原因を探る  (特にダメなのが頭の中でのトレース実行) やるべきことprintf “1”;  ・・・printf “2”;  ・・・printf “3”;出力が「1」であればこの間で止まっている 出力が「12」であればこの間で止まっている 上記を繰り返して、バグの発生箇所を特定する
  27. 27. 27/33 問題の切り分け (2/3)バグの発生箇所は、配列の領域外参照だったconst int N = 10;double data[N];・・・double func(int index){return data[index]; ← ここでindex=10だった}indexの値は0から9でないといけない→ どこかでおかしな値が入ったバグの発生箇所と、止まる箇所は一般に異なる バグの発見個所: 配列の領域外参照をした場所バグの発生個所: indexに不正な値が代入された場所 (本丸)
  28. 28. 28/33 問題の切り分け (3/3)おかしな値が代入された場所をどうやって探すか?→ assertを入れまくる(if文でも可) #include <assert.h>double func(int index){assert(index<N);・・・}Assertion failed: (i<10), function func, file test.cc, line 7. assertにひっかかると、以下のようなエラーが出て止まる assertには「満たすべき条件」を記載する ※普段からassertを入れているような人はこれを読む必要はありません これをたどって、不正な値が代入された場所を探す
  29. 29. 29/33 バグの実例 (1/2)double myrand_double (void){return (double)(rand())/(double)(RAND_MAX);}N未満の整数をランダムに返す関数が欲しかったrandは0からRAND_MAXまでの整数を返す関数(RAND_MAX=2147483647)それをRAND_MAXで割れば、0から1までの実数を返すはず? randは最高でRAND_MAXの値を返す→ myrand_intは低確率(21億分の1)でNを返すint myrand_int (const int N){return (int)(myrand_double()*N);}それをN倍してintにキャストすれば0からN-1を返すはず?
  30. 30. 30/33 バグの実例 (2/2)const int N = 10;double data[N];int index = myrand_int(N); ← ここがバグの原因…// (ずっと遠くで)return data[index]; ← 低確率で領域外参照が発生この種のバグの原因に「最初から思い至る」のは難しいprint文+assert文デバッグが有効 ※ちゃんとしたプログラマは最初からこういうことに気をつけます(僕はダメプログラマ)
  31. 31. 31/33 問題の切り分けとバージョン管理 (1/2)機能を追加したらバグった? → その機能を追加したことによるバグ?  もともとバグっていたものが顕在化?バージョン管理システムはタイムマシン Rev. 2とRev. 3のdiffを取れば、どこが原因かがすぐわかるバージョン管理していれば・・・ 開発時間軸 Rev. 1 Rev. 2 Rev. 3 Rev. 4 Rev. 5 (1)ここでバグ発覚 (3)実はここでバグ混入 (2)ここまでは動作することを確認(砦) デバッグ時間軸
  32. 32. 32/33 問題の切り分けとバージョン管理 (2/2)例:圧力測定ルーチンを追加したら、エネルギーが発散した ObservePressure MainKernel Ver. 1 ObserveEnergy Input A OK MainKernel Ver. 2 ObserveEnergy Input B NG MainKernel Ver. 1 ObserveEnergy Input B OK?NG? 測定ルーチン追加に伴い、計算カーネルも少し修正している→ その修正のせい?入力ファイルのせい? バグった?→ 圧力測定ルーチンを容疑者から除外バグらなかった?→ 圧力測定ルーチンと付随する修正が容疑者 圧力関連の修正前のリビジョンを取ってきてInput Bを食わせる バージョン管理をしていると、問題の切り分けが容易
  33. 33. 33/33 ・バグったら、再現するコードを保存する (現場保全)・いつバグが混入したか確認する (砦)・バグに関係のないルーチンを削除していく (問題の切り分け)・print文、assert文デバッグ (頭を使わない)まとめデバッグ (プログラミング)とは「ここまでは絶対大丈夫」という砦を築いていく作業 ※ 統合開発環境やデバッガを使っても良いが、とにかく原則として頭を使わないこと

×