演習 2 - ユーザー モード プロセスの割り当てを追跡する
ヒープの割り当ては、Heap API (HeapAlloc、HeapRealloc と、new、alloc、realloc、calloc などの C/C++ の割り当て) を介して直接行われ、3 種類のヒープを使って処理されます
メインライン NT ヒープ - 64 KB 未満のサイズの割り当て要求を処理します。
低断片化ヒープ - 固定サイズのブロックの割り当て要求を処理するサブセグメントで構成されています。
VirtualAlloc - 64 KB を超えるサイズの割り当て要求を処理します。
VirtualAlloc は VirtualAlloc API を使って直接行われる大規模な動的メモリ割り当てに使われます。 一般的な使用例としては、ビットマップやバッファーなどがあります。 VirtualAlloc を使ってページのブロックを予約し、VirtualAlloc を追加で呼び出して予約したブロックから個々のページをコミットすることができます。 こうすることで、プロセスで必要になるまで物理ストレージを使わずに、仮想アドレス空間の範囲を予約することができます。
この分野では 2 つの概念を理解する必要があります。
予約済みメモリ: 使用するアドレス範囲を予約しますが、メモリ リソースは取得しません。
コミット済みメモリ: アドレスが参照された場合に、物理メモリまたはページ ファイル領域のいずれかを使用できることを保証します。
この演習では、トレースを収集して、ユーザー モード プロセスがどのようにメモリを割り当てているかを調査する方法を学びます。
この演習では、MemoryTestApp.exe というダミーのテスト プロセスに焦点を当て、次の方法でメモリを割り当てます。
大きなメモリ バッファーをコミットする VirtualAlloc API。
小さなオブジェクトをインスタンス化する C++ new 演算子。
MemoryTestApp.exe はこちらからダウンロードできます。
手順 1: WPR を使って virtualAlloc/heap トレースを収集する
通常、大きなメモリ割り当てはプロセスの占有領域に影響するものであり、VirtualAlloc API によって処理されます。 すべての調査はここから始める必要がありますが、プロセスが小さいメモリ割り当てで誤動作する可能性もあります (たとえば、C++ の new 演算子を使ったメモリ リークなど)。 ヒープ トレースはこのような状況が発生したときに役立ちます。
手順 1.1: ヒープ トレースのためにシステムを準備する
ヒープ トレースは省略可能と考え、VirtualAlloc 分析でメモリ使用量の問題に関連する理由が判明しない場合に行うようにします。 ヒープ トレースによって生成されるトレースは大きくなる傾向があるため、調査対象となる個々のプロセスに限定してトレースを有効にすることをお勧めします。
対象となるプロセス (この場合は MemoryTestApp.exe) のレジストリ キーを追加します。これで、プロセスが作成されるたびにヒープ トレースが有効になります。
reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MemoryTestApp.exe" /v TracingFlags /t REG_DWORD /d 1 /f
手順 1.2: WPR を使ってトレースをキャプチャする
この手順では、WPR を使って VirtualAlloc と Heap のデータを含むトレースを収集します。
WPR を開き、トレースの構成を変更します。
VirtualAlloc と Heap のプロバイダーを選びます。
パフォーマンス シナリオとして [general](全般) を選びます。
[logging mode](ログ モード) として [general](全般) を選びます。
[Start](開始) をクリックしてトレースを開始します。
MemoryTestApp.exe を起動して、プロセスが終了するまで待ちます (30 秒ほどかかります)。
WPR に戻り、トレースを保存して、Windows Performance Analyzer (WPA) で開きます。
[Trace](トレース) メニューを開き、[Configure symbols path](シンボル パスの構成) を選びます。
- シンボル キャッシュのパスを指定します。 シンボルの詳細については、MSDN のシンボル サポートに関するページを参照してください。
[Trace](トレース) メニューを開き、[Load symbols](シンボルの読み込み) を選びます。
これで、MemoryTestApp.exe プロセスの有効期間中のすべてのメモリ割り当てパターンを含むトレースが作成されました。
手順 2: VirtualAlloc の動的割り当てを確認する
VirtualAlloc の詳細なデータは、WPA の 'VirtualAlloc Commit Lifetimes'(VirtualAlloc コミット有効期間) グラフを介して公開されます。 注目する主な列は次のとおりです。
列 | 説明 |
---|---|
Process | VirtualAlloc を介してメモリの割り当てを行うプロセスの名前。 |
Commit Stack (コミット スタック) | メモリが割り当てられるまでのコード パスを示す呼び出し履歴。 |
Commit Time (コミット時間) | メモリが割り当てられたときのタイムスタンプ。 |
Decommit Time (デコミット時間) | メモリが解放されたときのタイムスタンプ。 |
Impacting Size (影響するサイズ) | 未処理の割り当てサイズ、または選んだ時間間隔の開始時と終了時のサイズ差。 このサイズは、選んだビューポートに基づいて調整されます。 プロセスによって割り当てられたすべてのメモリが、WPA で視覚化された間隔の終わりまでに解放された場合、[Impacting Size](影響するサイズ) の値は 0 になります。 |
[サイズ] | 選んだ時間間隔中のすべての割り当ての累積合計。 |
次の手順で MemoryTestApp.exe を分析します。
Graph Explorer の [メモリ] カテゴリで [VirtualAlloc Commit Lifetimes](VirtualAlloc コミットの有効期間) グラフを見つけます。
[VirtualAlloc Commit Lifetimes](VirtualAlloc コミットの有効期間) を [分析] タブにドラッグ アンド ドロップします。
以下の列が表示されるように、表を整理します。 列ヘッダーを右クリックして、列を追加または削除します。
Process
Impacting Type (影響する型)
Commit Stack (コミット スタック)
Commit Time (コミット時間) と Decommit Time (デコミット時間)
Count
Impacting Size (影響するサイズ) と Size (サイズ)
プロセス一覧から MemoryTestApp.exe を見つけます。
MemoryTestApp.exe のみがグラフに表示されるようにフィルターを適用します。
- 右クリックして [Filter to Selection](フィルターして選択) を選びます。
分析ビューポートは次のように表示されます。
前の例では、次の 2 つの値が重要です。
126 MB の [Size](サイズ): これは MemoryTestApp.exe がその有効期間中に合計 125 MB を割り当てたことを示します。 これは、プロセスとその依存関係によって行われたすべての VirtualAlloc API 呼び出しの累積合計を表します。
0 MB の [Impacting Size](影響するサイズ): これは、プロセスによって割り当てられたすべてのメモリが、現在分析中の時間間隔の終わりまでに解放されたことを示します。 システムの定常状態のメモリ使用量が増加することはありませんでした。
手順 2.1: 定常状態のメモリ使用量を分析する
メモリの割り当てを調査するときは、"このシナリオで、定常状態のメモリ使用量が増加するのはなぜか" という疑問の答えを検討する必要があります。 MemoryTestApp.exe の例では、最初に約 10 MB の定常状態のメモリが割り当てられており、途中で 20 MB に増加していることがわかります。
この動作を調べるには、トレースの途中で急激な増加が発生する時間間隔付近にズームを絞り込みます。
ビューポートは次のようになります。
ご覧のとおり、[Impacting Size](影響するサイズ)が 10 MB になりました。 つまり、分析対象の時間間隔の開始時と終了時の間に、定常状態のメモリ使用量が 10 MB 増加しています。
列ヘッダーをクリックして、[Impacting Size](影響するサイズ) を基準に並べ替えます。
MemoryTestApp.exe の行 ([Process](プロセス) 列) を展開します。
[Impacting](影響する) の行 ([Impacting Type](影響する型) 列) を展開します。
プロセス Commit Stack 内を調べて、10 MB のメモリを割り当てた関数を見つけます。
この例では、MemoryTestApp.exe の Main 関数が VirtualAlloc を直接呼び出し、ワークロードの途中で 10 MB のメモリを割り当てています。 アプリケーション開発者が実際に行う場合、この割り当てが妥当かどうか、または定常状態のメモリ使用量の増加を最小限に抑えるためにコードを再編成できるかどうかを判断する必要があります。
これで、WPA のビューポートのズームを解除できます。
手順 2.2: 一時的な (またはピーク時の) メモリ使用量を分析する
メモリ割り当てを調査するときは、"シナリオのこの部分で、メモリ使用量が一時的に急増しているのはなぜか" という疑問の答えを検討する必要があります。 一時的な割り当ては、メモリ使用量が急増する原因となります。また、断片化を引き起こし、メモリが圧迫されているときに重要なコンテンツがシステムのスタンバイ キャッシュから押し出される可能性があります。
MemoryTest の例では、10 回のメモリ使用量の急増 (10 MB) がトレースに均等に分散して発生したことがわかります。
ズームを最後の 4 つの急増に絞り込むことで、目的の小さな領域に焦点を当て、関連性のない動作によるノイズを除去します。
ビューポートは次のようになります。
列ヘッダーをクリックして、[Size](サイズ) を基準に並べ替えます。
MemoryTestApp.exe の行 ([Process](プロセス) 列) を展開します。
[Transient](一時的) の行 ([Impacting Type](影響する型) 列) をクリックします。
- これで、ビューポート内でメモリ使用量が急増している箇所が青色で強調表示されます。
それぞれの列の値に注目してください。
[Count](カウント) = 4: これは、その時間間隔で 4 回の一時的なメモリ割り当てが行われたことを示します。
[Impacting Size](影響するサイズ) = 0 MB: これは、4 回の一時的なメモリの割り当てが時間間隔の終わりまでにすべて解放されたことを示します。
[Size](サイズ) = 40 MB: これは、4 回の一時的なメモリ割り当ての合計が 40 MB のメモリになることを示します。
プロセス Commit Stack 内を調べて、40 MB のメモリを割り当てた関数を見つけます。
この例では、MemoryTestApp.exe の Main 関数が Operation1 という関数を呼び出し、その関数が ManipulateTemporaryBuffer という関数を呼び出しています。 この ManipulateTemporaryBuffer 関数は、VirtualAlloc を 4 回直接呼び出し、毎回 10 MB のメモリ バッファーを生成して解放しています。 バッファーの使用時間はそれぞれ 100 ms です。 バッファーの割り当て時間と解放時間は [Commit Time](コミット時間) 列と [Decommit Time](デコミット時間) 列で表されます。
アプリケーション開発者が実際に行う場合、このように短時間の一時的なバッファーの割り当てが必要かどうか、または操作のために永続的なメモリ バッファーを使って置き換えることができるかどうかを判断します。
これで、WPA のビューポートのズームを解除できます。
手順 3: ヒープの動的割り当てを確認する
これまでの分析では、VirtualAlloc API で処理される大規模なメモリ割り当てにのみ注目してきました。 次の手順では、最初に収集したヒープ データを使って、プロセスによって行われた他の小さな割り当てに問題があるかどうかを判断します。
ヒープ データの詳細は、WPA の "Heap Allocations"(ヒープ割り当て) グラフで確認できます。 注目する主な列は次のとおりです。
列 | 説明 |
---|---|
Process | メモリ割り当てを実行しているプロセスの名前。 |
Handle | 割り当てに使われるヒープの識別子。 ヒープは作成できるので、プロセスには複数のヒープ ハンドルが存在する可能性があります。 |
スタック | メモリが割り当てられるまでのコード パスを示す呼び出し履歴。 |
Alloc Time (割り当て時間) | メモリが割り当てられたときのタイムスタンプ。 |
Impacting Size (影響するサイズ) | 未処理の割り当てサイズ、または選んだビューポートの開始時と終了時の差。 このサイズは、選んだ時間間隔に基づいて調整されます。 |
[サイズ] | すべての割り当てと割り当て解除の累積合計。 |
次の手順で MemoryTestApp.exe を分析します。
Graph Explorer の [Memory](メモリ) カテゴリで [Heap Allocations](ヒープ割り当て) グラフを見つけます。
[Heap Allocations](ヒープ割り当て) を [Analysis](分析) タブにドラッグ アンド ドロップします。
以下の列が表示されるように、表を整理します。
Process
Handle
Impacting Type (影響する型)
スタック
AllocTime
Count
Impacting Size (影響するサイズ) と Size (サイズ)
プロセス一覧から MemoryTestApp.exe を見つけます。
MemoryTestApp.exe のみがグラフに表示されるようにフィルターを適用します。
- 右クリックして [Filter to Selection](フィルターして選択) を選びます。
ビューポートは次のようになります。
この例では、ヒープの 1 つが時間の経過と共に一定の割合で着実にサイズが増加していることがわかります。 このヒープには 1200 回のメモリ割り当てが発生し、間隔の終了時には使用メモリが 130 KB に達しています。
トレースの途中にある、さらに小さな間隔 (たとえば 10 秒) にズーム インします。
([Impacting Size](影響するサイズ) 列に示されているように) 最大の割り当て量を示すヘッダーの [Handle](ハンドル) を展開します。
[Impacting](影響する) 型を展開します。
プロセス Stack 内を調べて、このすべてのメモリ割り当ての原因となった関数を見つけます。
この例では、MemoryTestApp.exe の Main 関数が InnerLoopOperation という関数を呼び出しています。 この InnerLoopOperation 関数は、C++ の new 演算子を使って 40 バイトのメモリを 319 回割り当てています。 このメモリは、プロセスが終了するまで割り当てられたままになります。
アプリケーション開発者が実際に行う場合は、この動作がメモリ リークの可能性を示しているかどうかを判断し、問題を解決する必要があります。
手順 4: テスト システムをクリーンアップする
分析が完了したら、レジストリをクリーンアップして、プロセスのヒープ トレースが無効になっていることを確認します。 管理者特権のコマンド プロンプトで次のコマンドを実行します。
reg delete "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\MemoryTestApp.exe" /v TracingFlags /f