連載
.NET&Windows Vistaへ広がるDirectXの世界

第6回 .NETアプリを軽快にするためのガベージ・コレクション講座

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2007)
2007/02/13
Page1 Page2 Page3 Page4

ゲームに見られるオブジェクト寿命の階層構造

 ここでは、XNA Game Studio Expressに付属するSpaceWarサンプルのような、古典的なアクション(シューティング)ゲームを想定する。イベント・ドリブン型が多いデスクトップ・アプリケーションとは異なり、このようなゲームでは、ディスプレイの画面書き換えに同期して、ゲームの更新と描画を1秒間に60回程度繰り返すループ構造になっていることが多い。これは、Webサーバや一般的なGUIアプリケーションのプログラミングとゲーム・プログラミングで大きく異なる点の1つだ。従って、GCとの付き合い方にも影響を及ぼすことになる。

 いくつか数字を当てはめてみることにしよう。リフレッシュ・レートを60Hz(=1秒間に60回更新する)と仮定し、1/60秒を1フレームと定義する。

  • 総プレイ時間=30分 : 108000フレーム
     ゲームを起動して、終了するまでの時間をこれぐらいと見積もろう。ゲーム本体に関連するオブジェクトは、おおむねこの程度の寿命を持つことになる。

  • 1回のゲーム=10分 : 36000フレーム
     1回のゲームは、開始して約10分でゲームオーバーになるとする。ステージを超えて引き継がれるような情報はこの程度の寿命を持つだろう。

  • 1ステージ=3分 : 10800フレーム
     1つのステージは、開始してこれぐらいの時間で終了すると仮定する。ステージごとに初期化するようなデータは、この程度の寿命を持つだろう。

  • 平均的な敵キャラクタの寿命=10秒 : 600フレーム
     アクション・ゲームには敵キャラクタがつきものだ。その典型的な寿命はゲームの内容によりけりだが、登場後約10秒で倒されるような敵を想定すれば、敵オブジェクトは平均してこれぐらいの寿命を持つことになる。

  • 平均的な敵の攻撃の寿命=5秒 : 300フレーム
     しばしば敵キャラクタはプレイヤーを武器で攻撃してくる。このときの攻撃も、プログラム内部では情報として管理する必要がある。その寿命はこれぐらいと考えよう。

  • 画面エフェクトの持続時間=3秒 : 180フレーム
     ゲームの進行に合わせて、爆発や閃光(せんこう)などさまざまなエフェクトが表示される。その持続時間に応じて、個々のエフェクトを表すオブジェクトの寿命が決まる。

 このように、たかがゲームといっても、寿命のスケールが異なるさまざまなオブジェクトが使用されることが分かる。アクション・ゲームのような動きの激しいゲームでは、一般的な傾向として、短寿命のオブジェクトほど大量に使用される傾向がある。

 さらに、各フレームの処理にも同様の階層構造が見られる。フレーム処理開始から終了まで保持しておくべき情報(例えばゲーム・パッドの内容や更新開始時刻など)は、1フレームと同様、つまり1/60秒程度の寿命を持つだろう。

 フレーム内での更新作業(衝突判定や3D形状のアニメーション処理)の開始から終了までに生存する情報は、1フレームよりは短い寿命を持つ。例えば衝突判定に用いる内部的なデータ構造などは、衝突判定開始時に作られ、判定結果を返すときには不要になる。その寿命は、ミリ秒かそれ以下だ。

 そして、こういったさまざまな処理を記述するうえで使用される小さなユーティリティ関数は、やはり内部で中間オブジェクトを使用している。文字列処理で発生する中間文字列や、計算で一時的に使用する配列など、ある関数内でごく短い間だけ必要となるオブジェクトたちだ。これらはミリ秒からマイクロ秒以下の単位で、ごく短時間にのみ必要とされる。

 定常状態を考えれば、フレーム処理開始直前または直後が、最もオブジェクト数が少ない状態ということになるだろう。それがステージ開始直前や、ゲーム開始直前であれば、必要なオブジェクト数はより少ないと予想される。

 ゲーム・プログラムの特徴は、こういったオブジェクトの生成と破棄が定常的に繰り返されているところにある。このような特徴は、GCの働きにどのように作用するだろうか? それにはまずGCの発生頻度に注目してみよう。

GCの発生頻度

 GCの発生頻度は、アプリケーションがGCヒープをどれだけ早く消費するかに比例する。軽量の第0世代GCで回収できるオブジェクトは、このGC発生間隔よりも短い寿命である必要がある。すなわち、GCの世界での「短い寿命」とは、第0世代GCの発生間隔に対応して、相対的に決まるものだ。

 もし、ある小さなユーティリティ関数が大量のオブジェクトを生成し、その総量がMbytes単位であるのなら、.NET CLRは恐らく第0世代GCを発生させるだろう。これではゲーム・ループ中に「System.GC.Collect(0)」と書いたのと変わらない。すなわち、ユーティリティ関数の呼び出し元に存在するようなオブジェクトはほとんどすべて、「短寿命ではない」という扱いになり、その回収は第1世代以上のGCの発生を待つこととなる。

 逆に、プログラムを工夫してGCヒープからのメモリ確保を極力避けていれば、GCの発生頻度を1フレーム、すなわち1/60秒よりも遅らせることができるかもしれない。この場合、1フレーム以下の寿命を持つオブジェクトの大多数は、「短寿命オブジェクト」として軽量な第0世代GCで回収されることが期待できる。

 そして最も停止時間の長い第2世代GCの発生頻度は、どれぐらいの割合でオブジェクトが第2世代にたまっていくかに大きく依存する。第2世代GCの発生間隔を、ゲームの1ステージである18000フレームに比べて十分長くできれば理想的だ。

 なお、.NET CLRは85Kbytes以上のオブジェクトをLarge Object Heap(LOH)と呼ばれる特殊なヒープに配置する。LOHに配置されたオブジェクトはコンパクト化で移動されることはなく、また最初から第2世代という特徴を持つ。つまり、各フレーム処理でLOHからの確保を繰り返せば、第2世代GCの発生頻度もそれだけ高くなる。

 もう1つの注意点は、マルチスレッドに関するものだ。

 これまでは暗にシングルスレッドと仮定して話を進めてきた。しかし、Xbox 360は3つのCPUコアがそれぞれハイパー・スレッディングに対応しており、合計6つのハードウェア・スレッドが存在することになる。XNAではこのうち4つを同時に使用することができる。ゲームのメイン・ループを記述するメイン・スレッドがGCヒープの消費速度を抑えていても、別スレッドが頻繁にGCヒープからメモリ確保を行っていると、やはりGCの発生頻度は高くなるだろう。

オブジェクトの生成レート

 では、どれぐらいのオブジェクト生成レートなら許されるのだろうか?

 CLRの開発者でGCについて詳しいRico Mariani氏は、パフォーマンス・カウンタの「%Time in GC」が30%近くを示しているような状況では“Mid life crisis”が起きている可能性が高いと述べている。Mid life crisisとは、本来第1世代までで回収されてほしいオブジェクトの多くが、第2世代まで生き残ってしまう現象だ。

 先ほど紹介した「Taming the CLR: How to Write Real-Time Managed Code」で、Mariani氏はこう述べている。「20〜40Mbytes/sec程度であれば、メモリ管理のオーバーヘッドも望ましいものだろう。60frame/sec(=60FPS)のゲームならだいたい500Kbytes/frameとなる、あるいはそこそこのサイズのオブジェクトで1フレーム当たり2万個だ」。.NET CLRの典型的な第0世代GCヒープのサイズから逆算すれば、これはちょうど1フレームの処理で毎回はGCが発生しないというという値になる。

 世代別GCを採用していない.NET CF CLRやXbox 360 CLRでは、世代の昇格という要素が存在しない代わりに、毎回のGCは.NET CLRの第0、第1世代GCほど低コストではないと予想される。GCヒープを消費するようなオブジェクトの生成レートを低く抑えることは、やはりパフォーマンス上有効だろう。XNA Remote Performance Monitorの、“Garbage Collection (GC)”と“GC Compaction”のカウンタが、どれぐらいの速度で増加するかに注意してほしい。


 INDEX
  .NET&Windows Vistaへ広がるDirectXの世界
  第6回 .NETアプリを軽快にするためのガベージ・コレクション講座
    1.GCの影響を軽減する必要性
    2.GCがアプリケーションに与える影響
  3.GCの発生頻度を抑えるための対策
    4.値型でGCヒープの消費を抑える
 
インデックス・ページヘ  「.NET&Windows Vistaへ広がるDirectXの世界」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間