メモリトラフィックを最適化する
サンプルアプリケーション | |
このチュートリアルでは、dotMemory を使用してアプリケーションのメモリ使用を最適化する方法を説明します。
「メモリ使用量の最適化」とは何を意味するのでしょうか? オペレーティングシステム内の他のプロセスと同様に、GC(ガベージコレクタ)はシステムリソースを消費します。 仕組みは簡単です。GC がコレクションを多く実行するほど、CPU オーバーヘッドが大きくなり、アプリケーションのパフォーマンスが低下します。 これは通常、アプリケーションが限られた期間だけ必要な大量のオブジェクトを割り当てる場合に発生します。
このような問題を特定し分析するには、いわゆる メモリトラフィックを調べる必要があります。 トラフィック情報は、特定の時間間隔中に割り当てられ、解放されたオブジェクト(およびメモリ)の数を示します。 アプリケーションで過剰な割り当てを特定し、dotMemory を使用して取り除く方法を見てみましょう。
サンプルアプリケーション
従来、このチュートリアルで使用するサンプルアプリケーションは、コンウェイのライフゲームです。 始める前に、github(英語) からアプリケーションをダウンロードしてください。

このアプリケーションは多数のオブジェクト(セル)で動作するため、これらのオブジェクトがどのように割り当てられ、収集されるかのダイナミクスを調べることは興味深いでしょう。
ステップ 1. dotMemory を実行
Visual Studio で Game of Life ソリューションを開きます。
メニュー を使用して dotMemory を実行します。

開いた プロファイラ設定 ウィンドウで、 開始からメモリ割り当てとトラフィックデータを収集する を選択します。 これにより、dotMemory はアプリ起動後にプロファイリング情報の収集を開始します。 これは、オプションを指定した後のウィンドウの外観です。

プロファイリングセッションを開始するには、 実行 をクリックします。 これによりアプリケーションが起動し、dotMemory のメインの 分析 #1 ページが開きます:

タイムラインを表示するには、dotMemory のメインウィンドウに切り替えます。 タイムラインには、アプリケーションのメモリ使用量がリアルタイムで表示されます。 具体的には、アンマネージメモリ *、Gen0、Gen1、Gen2 ヒープおよびラージオブジェクトヒープの現在のサイズの詳細を提供します。 生命のゲームが始まるまでは、メモリの消費は止まっています。

ステップ 2. スナップショットを取得する
アプリケーションの起動後、メモリスナップショットの取得を開始できます。 アプリケーションの動作のダイナミックスを調べたいため、少なくとも 2 つのスナップショットを取る必要があります。 スナップショットを取得するまでの時間間隔は、さらにメモリトラフィック分析の対象となります。
当然のことながら、分配の大部分が発生した場合、Game of Life の操作の間、両方のスナップショットを取らなければなりません。 Game of Life の第 30 世代と第 100 世代の第 2 世代のスナップショットを撮りましょう。
アプリケーションの 開始 ボタンを使用してゲームを開始します。
世代カウンタ(アプリの右上)が 30* に達すると、dotMemory の スナップショットを取得 ボタンをクリックします。

タイムラインを見ると、アプリケーションがリアルタイムでメモリをどのように消費するかが分かります。 アプリケーションが新しいオブジェクトを割り当てると、メモリ消費量が増加します(Gen0 ダイアグラムが大きくなります)。 ガベージコレクションが行われると、メモリ消費量が減少します。 その結果、タイムラインは鋸のようなパターンに従います。
世代カウンタが 100 に達すると、もう一度 dotMemory の スナップショットを取得 ボタンを使用して 1 つのスナップショットを取得します。
Game of Life アプリを終了してプロファイリングセッションを終了します。 メインページには 2 つのスナップショットが含まれています。

ステップ 3. メモリトラフィックを分析する
ここでは、スナップショットを取得するまでの時間間隔でのメモリトラフィックを見ていきます。
両方のスナップショットが比較領域に追加されていることを確認します(比較に追加 が両方とも選択されています)。

比較領域で メモリトラフィックを表示する をクリックすると、 メモリトラフィック ビューが開きます。 ビューには、スナップショット #1 とスナップショット #2 の間に作成された特定のタイプのオブジェクトの数が表示されます。

リストを参照してください。
GameOfLife.Cell* objects の割り当てのために、メモリトラフィック全体の約 50% である 27 MB 以上が発生します。 同時に、これらのセルの大部分、26+ MB も同様に収集された。 Game of Life の全期間にわたってセルが存在しなければならないため、非常に奇妙です。 これらのコレクションがアプリケーションのパフォーマンスを傷つけていることは間違いありません。 これらのCellオブジェクトがどこから来たのか調べてみましょう。GameOfLife.Cellクラスの行をクリックします。 この画面の下部にあるリストは、オブジェクトを作成した機能(バックトレース)を示しています。 どうやら、これはGridクラスのCalculateNextGeneration()メソッドです。 コード内で見つけよう。
Visual Studio で GameOfLife ソリューションを開きます。
Gridクラスの実装を含む Grid.cs ファイルを開きます。
CalculateNextGeneration(int row, int column)メソッドを探します。public Cell CalculateNextGeneration(int row, int column) { bool alive; int count, age; alive = _cells[row, column].IsAlive; age = _cells[row, column].Age; count = CountNeighbors(row, column); if (alive && count < 2) return new Cell(row, column, 0, false); if (alive && (count == 2 || count == 3)) { _cells[row, column].Age++; return new Cell(row, column, _cells[row, column].Age, true); } if (alive && count > 3) return new Cell(row, column, 0, false); if (!alive && count == 3) return new Cell(row, column, 0, true); return new Cell(row, column, 0, false); }このメソッドは、ライフゲームの各次世代ごとに
Cellオブジェクトを計算して返すようです。 しかし、これだけでは大量のメモリトラフィックの説明にはなりません。 dotMemory に戻り、どの関数がCalculateNextGenerationメソッドを呼び出しているか確認しましょう。dotMemory では、
CalculateNextGenerationメソッドを展開して、スタック内の次の関数を表示します。GridクラスのUpdateメソッドです。
コード内でこのメソッドを見つける:
public void Update() { for (int i = 0; i < SizeX; i++) { for (int j = 0; j < SizeY; j++) { _nextGenerationCells[i, j] = CalculateNextGeneration(i,j); } } UpdateToNextGeneration(); }これで、膨大なメモリトラフィックの原因が明らかになりました。 これは、ライフゲームの次世代のセルを管理する
nextGenerationCells型のCell配列です。 各世代の更新ごとに、この配列内のセルが新しいものと置き換えられます。 前の世代に残されたセルは不要となり、しばらく後にGCによって回収されます。 アプリケーションの全期間にわたってこの配列が存在しているため、毎回新しいセルで_nextGenerationCells配列を埋める必要はありません。 大量のメモリトラフィックをなくすには、新たなセルを作成する代わりに、既存のセルのプロパティを新しい値で更新する必要があります。 これをコードで実装しましょう。実際、例題用アプリケーションにはすでに
CalculateNextGenerationメソッドの必要な実装が含まれています。 このメソッドは、参照渡しされたセルのIsAliveフィールドとAgeフィールドを更新します:public void CalculateNextGeneration(int row, int column, ref bool isAlive, ref int age) { ... }この問題を修正するには、このメソッドを使って
_nextGenerationCells配列を更新しているUpdate()内の行のコメント解除を行ってください。 最後に、Update()メソッドは次のようになります:public void Update() { bool alive = false; int age = 0; for (int i = 0; i < SizeX; i++) { for (int j = 0; j < SizeY; j++) { CalculateNextGeneration(i, j, ref alive, ref age); _nextGenerationCells[i, j].IsAlive = alive; _nextGenerationCells[i, j].Age = age; } } UpdateToNextGeneration(); }では、これらの変更を適用して、メモリトラフィックへどのような影響があるか確認しましょう。
アプリケーションをもう一度ビルドします。 Step 1 の手順を繰り返してください。 dotMemory を実行し、 Step 2 も実行してください。 スナップショットを取得して新しいスナップショットを用意しましょう。
メモリトラフィック ビューを開き、収集したスナップショット間のメモリトラフィックを確認します(Step 3 のサブステップ 1 および 2 で説明されています: メモリトラフィックを分析:

GameOfLife.Cellクラスはもうリストにはありません ! その結果、全体のトラフィックが 40% 減少し(33MB まで)、これは非常に優れた最適化です。