メモリトラフィックを最適化する
サンプルアプリケーション | |
このチュートリアルでは、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); }このメソッドは、次世代の Game of Life の
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(); }これにより、メモリトラフィックが多い原因がついに明らかになります。次世代のライフゲーム用のセルを格納する
Cell
タイプのnextGenerationCells
配列があります。世代が更新されるたびに、この配列のセルは新しいセルに置き換えられます。前世代から残ったセルは不要になり、しばらくすると 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(); }これらの変更を適用して、それらがメモリトラフィックにどのように影響するかを確認しましょう。
もう一度アプリケーションをビルドします。ステップ 1. dotMemory を実行とステップ 2. スナップショットを取得するで説明した手順を繰り返して、2 つの新しいスナップショットを取得します。
メモリトラフィックビューを開いて、収集したスナップショット間のメモリトラフィックを確認します(ステップ 3. メモリトラフィックを分析するサブステップ 1 と 2 で説明します)。
GameOfLife.Cell
クラスはもうリストにはありません ! その結果、全体のトラフィックが 40% 減少し(33MB まで)、これは非常に優れた最適化です。
関連ページ:
API を使用したプロファイリングセッションの制御
プロファイリング API は、プロファイリングプロセスを制御できるようにする多数のクラスを提供します。例: アプリケーションのコードから、次のことができます。メモリのスナップショットを取得:,、メモリ割り当てデータの収集を有効または無効にします:,、ガベージコレクションを強制する:,、API クラスの詳細については、API 参照を参照してください。プロファイリング API を使用する必要がある主なシナリオは 2 つあります。コードの特定部分のプロファイリング、自己プロファイルアプリケーション。この...
メモリリークを見つける
サンプルアプリケーション人生ゲーム、このチュートリアルでは、dotMemory を使用してアプリケーションでメモリリークを見つけて修正する方法を説明します。しかし、前に進む前に、メモリリークが何であるかに同意しましょう。メモリリークとは何ですか? :最も一般的な定義によれば、メモリリークは、「オブジェクトがメモリに格納されているが、実行中のコードからアクセスできない」という誤ったメモリ管理の結果です。さらに、「メモリリークは時間とともに増加し、クリーンアップされなければシステムは最終的にメモリ不...