タイムルーラー
注:今回紹介するコンポーネントはデバッグサンプルに入っています。
時を測る
前回紹介したFPSカウンターではゲーム全体のパフォーマンスを測定するには使えますが、どの処理がどれだけ時間が掛かっているかを判定するには不向きです。
例えば、ねこが大勢の敵をなぎ倒す「ねこ無双」というゲームを作っていて、敵が沢山出てきた時に処理落ちになった場合に最適化をする必要があったとします。
最適化ルールその3:「最も負荷の高い部分(ボトルネック)の処理を最適化する」
これは当然の話で、1フレーム内で10%しか消費していない処理よりも、50%消費している部分を最適化する方が効果的です。では、この高負荷の処理を発見するにはどうしたら良いでしょうか?ねこ無双の場合だと、敵が増えると処理落ちがするんだから真っ先に思いつくのは敵AI、敵のアニメーション、敵のコリジョン判定、敵の描画処理部分です。
しかし、必ずしもそうとは限りません。もしかしたら、敵の数ではなく戦っている場面の描画部分がもともと負荷が多かったのかもしれないし、敵を表示するレーダー部分かもしれません。
このボトルネックを発見するのに有用なのがリアルタイムプロファイラー、TimeRuler(タイムルーラー)です。TimeRulerでは、複数の処理時間を測定することができ、その結果をグラフでリアルタイムに表示してくれます。
上図の例では、青い部分が更新処理、黄色い部分が描画処理になっています。この状態で最適化する場合は更新処理部分を最適化するのが効果的だと言うことが一目で判ります。
アルゴリズムのチェック
理想的なアルゴリズムは計算量のオーダーがO(n)、もしくはO(n log n)になることです。例えば、敵を10体出現させたときの処理時間が1msだった場合、敵が100体になった時の処理時間が10ms~20ms以内に収まっていれば理想的なアルゴリズムを使っているということになります。逆に敵100体の処理時間が1,000msになってしまうのは、計算量オーダーがO(n^2)になっているアルゴリズムを使っている可能性があります。良くあるケースでは、コリジョン判定を単純な多重ループにしている時などです。
特に最近のゲームでは大量のオブジェクト数を処理するので、アルゴリズムの選択を間違えると、あっという間に処理が間に合わなくなってしまうので注意が必要です。
TimeRulerを使ってオブジェクトの数を10,20,...100と順々に増やしていき、それぞれの処理時間を測定してグラフにすることで問題のあるアルゴリズム部分を発見することができます。
瞬間最大負荷を測る
TimeRulerはリアルタイムプロファイラーなので、瞬間的な負荷も視覚的に見ることができます。例えば同じフレーム内で大量の敵を一気に発生させると、敵の初期化処理に時間が掛かりすぎて、そのフレームだけ処理落ちするということがあります。こういった瞬間的な負荷はFPSカウンターで発見することは不可能ですが、TimeRulerを使うとグラフが一瞬大きくブレるので判ります。
また、TimeRulerを表示した状態でビデオに録画したり、動画をキャプチャーしておけば、こういった瞬間的な負荷を細かく調べることもできます。
TimeRulerを使う為の準備
TimeRulerコンポーネントはDebugManagerにあるフォントを使用するので、TimeRulerを追加する前にDebugManagerコンポーネントを追加する必要があります。
// デバッグマネージャーの初期化と追加
debugManager = new DebugManager( this );
Components.Add( debugManager );
また、必須ではありませんが、TimeRulerはインスタンス生成時にGame.ServicesにIDebugCommandHostインターフェースが追加されていると、trデバッグコマンドを登録します。DeubgCommandUIはIDebugCommandHostインターフェースを実装しているので、このコンポーネントを追加した後にTimeRulerを追加することでtrコマンドを使うことができます。
// デバッグマコマンドUIの初期化と追加
debugCommandUI = new DebugCommandUI( this );
// デバッグコマンドUIを最上面に表示させる為にDrawOrderを変更する
debugCommandUI.DrawOrder = 100;
Components.Add( debugCommandUI );
デバッグコマンドUIをTabキーを押して表示させた後に、trとタイプするとTimerRulerの表示/非表示の切り替え、"tr on"で表示、"tr off"で非表示にすることができます。
また、trコマンドには以下のオプションがあります。
log | ログの表示/非表示の切り替え。 例) tr log |
reset | ログ履歴の初期化 例) tr reset |
frame | TimeRulerのバーの長さをフレーム単位で指定する(既定値:1) 例) tr frame:2 |
プログラム側でも、これらの操作はできるようになっていて、表示の切り替えはTimeRuler.Visibleプロパティ、Logの表示はTimeRuler.ShowLogプロパティ、表示フレーム数はTimeRuler.TargetSampleFramesプロパティ、そしてログのリセットはTimeRuler.ResetLogメソッドを呼ぶことでできます。
最後に、TimeRulerをComponentsに追加することでTimeRulerが画面下に表示されるようになります。ただし、DebugCommandUIが先に登録されていると、初期状態では表示されません。
// タイムルーラーの初期化と追加
timerRuler = new TimeRuler( this );
Components.Add( timerRuler );
TimeRulerの使い方
上記の初期化を終えれば、TimeRulerを使う準備ができました。まず、TimeRulerにフレームの開始時点を知らせるStartFrameメソッドを呼びます。場所はGameクラスのUpdateメソッドの先頭に記述するのが良いでしょう。
// タイムルーラーにフレーム開始を伝える
timerRuler.StartFrame();
次に測定したい部分をBeinMarkとEndMarkメソッドで囲みます。BeginMarkメソッドには表示するバーのインデックス値(0~7)、マーカー名、バーに表示されるときの色を指定し、EndMarkではバーのインデックス値とマーカー名を指定します。マーカー名はCaseセンシティブなので、大文字小文字が違うと違うマーカー名と認識されることに注意してください。バーのインデックス値は省略するとインデックス値を0として呼び出した事になります。
// 更新期間、"Update"の測定開始
timerRuler.BeginMark( 0, "Update", Color.Blue );
// Updateの処理をここでする
...
// 更新期間、"Update"の測定終了
timerRuler.EndMark( 0, "Update" );
BeginMarkは以下のコードのように階層的に呼び出すこともできます。デフォルトでは最大32階層になっていますが、TimeRuler.MaxNestCall定数値を変更することができます。ただし、Begin A, Begin B, End B, End Aのように呼べますが、Begin A, Begin B, End A, End Bのように呼ぶことはできません。
timerRuler.BeginMark( "Update", Color.Blue );
timerRuler.BeginMark( "Character Update", Color.Azure );
timerRuler.BeginMark( "Character Animation", Color.BurlyWood );
// キャラクターアニメーション処理
...
timerRuler.EndMark( "Character Animation" );
timerRuler.BeginMark( "Character Collision", Color.DarkOrchid );
// キャラクターコリジョン処理
...
timerRuler.EndMark( "Character Collision" );
timerRuler.EndMark( "Character Update" );
timerRuler.EndMark( "Update" );
TRACE定数でTimeRuler機能のコントロール
TimeRulerはその性質上リリースビルドでも動作するようになっていますが、実際にゲームをリリースするときには実行する必要はありません。ゲームコード内に埋め込んだBegin/EndMarkを手で消したりするのはあまりにも不効率過ぎるので、簡単にそれらのメソッドの呼び出しを制御するためにTimeRulerの殆どのメソッドにはConditional属性を使ってTRACE定数が宣言されているときのみに呼び出されるようになっています。TRACE定数はプロジェクトプロパティのビルドタブにある「TRACE定数の定義」のチェックボックスによってコントロールすることができます。
C/C++ではコンパイラ最適化の一つとして呼び出し先が空の関数の場合には関数呼び出しのコードを生成しないようになっていますが、.Netでは呼び出するメソッドが空のメソッドであっても、メソッド呼び出しコードを生成します。代わりにConditional属性を使い、指定された定数が宣言されていない場合は、そのメソッドに使われた引数も含めて呼び出しコードを生成しないようになっています。
マシン性能のクセを知る
ここではTimeRulerなんていう格好つけた名前をつけてはいますが、同様の仕組みは8ビット機時代から使われていたものです。例えばPCエンジンでは、あるパレットの色を変更するとTVの画面外の色を変更することができるので、処理毎に色を変えることで画面の両脇に縦に伸びるバーを作ることができました。
実装方法は変わりましたが、バーが振り切らないようにゲームを作るという目的は今も昔も一緒です。特に、最近のシステムでは幾重ものパイプラインステージやキャッシュといった複雑な仕組みがあるので、必ずしもコード量が実行速度に比例するわけではありません。ですから、常にTimeRulerを使うことでどの処理がどれくらいの負荷になるのかを実感できることができます。
次回はデバッグコマンドを紹介します。
Comments
Anonymous
December 29, 2008
PingBack from http://www.codedstyle.com/%e3%82%bf%e3%82%a4%e3%83%a0%e3%83%ab%e3%83%bc%e3%83%a9%e3%83%bc/Anonymous
May 03, 2009
ブログ主さん、初めまして。 早速なのですが一つ質問が。 http://blogs.msdn.com/ito/archive/2009/03/25/how-gpus-works-01.aspx の記事だと「Draw()で行うようなDrawPrimitives()の呼び出しはあくまでGPUに渡すコマンドを生成しているだけで、実際のGPUによる描画は(XNAでは通常ラップされている)Present()呼び出しのあとに行われているとありますが、だとするとこのタイムルーラーはあくまでCPUの処理を測っているだけで、GPUの処理は測っていないということですか?Anonymous
May 03, 2009
先程質問したものです。 失礼しました、サンプルだとtimerRuler.EndMark( "Draw" )はbase.Draw( gameTime )のあとで呼ばれていますね。ということはbase.Draw()の中でPresent()が呼び出されていて、Present()がGPU処理完了までブロックするので、そこでEndMark()することでGPUの処理が測定できている、という仕組みだったのでしょうか。Anonymous
May 03, 2009
タイムルーラーはCPUの処理時間の測定しかできません。 また、Presentはbase.Draw()の中ではなく、EndDrawメソッドの中で行われます。Anonymous
May 04, 2009
回答ありがとうございます。図を見直しても、やはりPresent()自体、GPUを待っているわけではないですね。 …ズバリ、GPUの描画完了タイミングを知る方法はないのでしょうか?Anonymous
May 04, 2009
Presenとは前フレームのGPU処理が終わっていない場合は、その処理が終わるまで待ちます。ただ、Presentに掛かった時間=GPUの待ち時間ではありません。Presentの処理時間にはGPUへの命令発行などのも含まれる点に注意しないといけません。 >GPUの描画完了タイミングを知る方法はないのでしょうか? Windows上なら、Vista以降のOS上ではPIXを使ってGPUイベントのタイミングを知ることができます。 http://msdn.microsoft.com/en-us/library/bb173112(VS.85).aspx 残念ながら、Xbox 360上ではタイミングを知る方法は現在提供されていません。Anonymous
May 05, 2009
回答ありがとうございました。じつはXP環境だったのでPIXでのGPU測定ができず、「アプリ自体で完了タイミングを取得する方法がないものか」と思っていたのです。が、GPUの負荷測定はやっぱりVista&PIXでないとできないのですね。