メモリの問題を修正
メモリの割り当てはプログラムに不可欠なのに、なぜ DPA ではそれを課題の指標にしているのですか? 問題はパフォーマンス的に非常に低コストなメモリの割り当てではなく、システムリソースを多く必要とする可能性があるガベージコレクション(GC)にあります。 多くのメモリを割り当てるほど、今後回収する必要も増えます。 .NET のメモリ管理について簡単に復習したい場合は、 JetBrains dotMemory ドキュメントを参照してください。
DPA は、 クロージャ、 スモールオブジェクトヒープへの 割り当て、およびラージオブジェクトヒープへの割り当てに関連するメモリの問題を検出します。 問題は、 動的プログラム分析 ウィンドウの メモリの割り当て タブにグループ化されています。

この章では、このような問題につながる可能性のあるコード設計の例と、それを修正する方法に関するヒントを紹介します。
また、DPA と併せて ヒープアロケーションビューアープラグインの利用も強くおすすめします。 このプラグインは、コード内でメモリが割り当てられるすべての場所をハイライト表示します。 これは DPA にとって理想的な組み合わせです。 プラグインが割り当てとその理由を示す一方で、DPA はその割り当てが本当に課題かどうかを判定します。

クロージャ オブジェクト
- これは何ですか?
クロージャは、 ラムダ式や LINQ クエリにコンテキストを渡す場合に発生します。 クロージャを処理するために、コンパイラーは特別なクラスとデリゲートメソッド:
<>c__DisplayClass...とFunc<...>を作成します。 LINQ も呼び出すたびに列挙子を生成することに注意してください。- 見つけ方は?
DPA は、
<>c__DisplayClass...の割り当てを Closure 問題としてマークし、Func<...>の割り当ては 小さなオブジェクトヒープ 問題としてマークします。デフォルトのしきい値は 50MB です。
- 修正方法は?
通常、このコードは コンテキストがラムダに渡されないように書き換えられます。 LINQ の場合、唯一の 解決策はそれらを使用しないことです。
ラムダ式の閉包
ラムダ式は、特定の状況でコードを大幅に簡略化できる非常に強力な .NET 機能です。 残念ながら、誤って使用すると、ラムダはアプリのパフォーマンスに大きな影響を与える可能性があります。
例を考えてみましょう。 ここでは、文字列のリストにフィルターを適用するために使用される Filter メソッドを持ついくつかの StringUtils クラスがあります。
このメソッドを使って、指定した長さより長い文字列をフィルターで除外します。 このフィルターはラムダを使って条件引数に渡します。 結果となるコード(コンパイル後)は、ラムダがクロージャを含むかどうかによって変わることに注意してください。 クロージャは、たとえばラムダを呼び出すメソッド内で宣言したローカル変数など、ラムダに渡されるコンテキストのことです。 この場合、コンテキストは希望する文字列の長さです。
フィルターを使用する最も明白な方法は、引数として文字列の長さを渡すことです: length
上記のコードを JetBrains dotPeek などで逆コンパイルすると、次のようになります。
コンパイラーは、
b__0メソッドにラムダを格納する<>c__DisplayClass0_0クラスを作成します。ラムダを含むメソッドを実行すると、最初に、
<>c__DisplayClass0_0クラスの新しいインスタンスが作成されます。次に、
Func<>デリゲートを作成してラムダを呼び出します:new Func<string, bool>((object) cDisplayClass00, __methodptr(b__0))).Count)
ラムダを呼び出すたびに、新しいクラスインスタンスと新しいデリゲートインスタンスが割り当てられます。 ラムダによって発生する割り当ては、ポイント 2 と 3 の合計です。 ラムダがホットパス上にある場合(頻繁に呼び出されます)、大量のメモリが割り当てられます。
DPA がこれを示す方法
ラムダ 10000 回を含むメソッドを呼び出しましょう。 DPA は <>c__DisplayClass0_0 クラス(上記のリストのポイント 1)の作成を検出し、これらの割り当てを Closure 問題としてマークします。 クロージャーの場合、DPA はメソッドの最初の開き括弧を問題としてハイライトします。

問題は次のようになります(<>c__DisplayClass0_0 タイプの割り当て)。

現在、ラムダデリゲート(上記のリストのポイント 2)の割り当ては、 Closure ではなく 小さなオブジェクトヒープ の問題としてマークされています。

問題は次のようになります(Func<...> タイプの割り当て)。

直す方法
ラムダを使用する際の主な戦略は、クロージャーを回避することです。 このような場合、特別な <>c__DisplayClass0_0 クラスのインスタンスがキャッシュされ、作成されたデリゲートは静的になります。 その結果、追加のメモリ割り当てはありません。 この例では、1 つの解決策として、パラメーター length をラムダではなく Filter メソッドに渡します。 修正は次のようになります。
今プロジェクトを実行して DPA の結果を確認すると、クロージャは検出されません。 <>c__DisplayClass0_0 も Func<> インスタンスも作成されていません。


ラムダの代わりにメソッドグループ
2006 年に C# 2.0 で「メソッド グループ変換」機能が導入され、メソッドをデリゲートに割り当てる際の構文が簡素化されました。 この場合、ラムダの代わりにメソッドを使い、そのメソッドを引数として渡すことができます。
ここにはクロージャがないため、このアプローチでは追加のメモリ割り当てが生成されるでしょうか ? 残念ながらそうです。 DPA 分析結果を見ると、 FilterLongString() が呼び出されるたびにメソッドが Func<string, bool> の新しいインスタンスを作成することがわかります。 10,000 回呼び出されると、10,000 個のインスタンスが生成されます。


メソッドグループを使用する場合は、クロージャーでラムダを使用する場合と同じ注意を払ってください。 ホットパスに留まると、大量のメモリトラフィックが発生する可能性があります。
LINQ クエリのクロージャー
LINQ クエリとラムダ式の概念は密接に関連しており、「内部」での実装は非常に似ています。 これは、 ラムダに関して論じたすべての懸念は LINQ にも当てはまることを意味します。 LINQ クエリにクロージャーが含まれている場合、コンパイラーは対応するデリゲートをキャッシュしません。 たとえば、次のようになります。
DPA がこれを示す方法
threshold パラメーターはクエリによって取得されるため、メソッドが呼び出されるたびに、そのデリゲート(Func<String, Boolean> )と対応する <>c__DisplayClass0_0 インスタンスが作成されます。 GetLongNames() 10000 回呼び出すと、DPA は対応する問題を検出します。

LINQ クエリでの列挙子の割り当て
残念ながら、LINQ を使う際に避けるべき落とし穴が 1 つあります。 LINQ クエリ(他のクエリ同様)は、あるデータコレクションに対する反復処理を想定しており、そのためイテレーターの作成を前提としています。 この推論の流れはすでにおなじみでしょう。この LINQ クエリがホットパス上にある場合、イテレーターの継続的な割り当てにより大量のメモリトラフィックが生成されます。 前の例をクロージャを含まないように変更しましょう:
GetLongNames 10000 回呼び出すと、メモリ割り当てが残ります。

動的プログラム分析 ウィンドウには、 小さなオブジェクトヒープ タイプの次の問題が表示されます。

この種の課題は簡単に見分けることができます。クラス名に必ず Iterator が含まれています。
直す方法
残念ながら、ここでの唯一の答えは、ホットパスで LINQ クエリを使用しないことです。 ほとんどの場合、LINQ クエリは foreach に置き換えることができます。 この例では、クロージャと列挙子の割り当てを生成しないユニバーサル修正は次のようになります。
小さなオブジェクトヒープ
この問題タイプの別名は「未分類」です。 厳密に言えば、 小さなオブジェクトヒープ しきい値を超えるアプリケーションによって行われたすべてのメモリ割り当てが含まれます。 今後のリリースでは、より多くのメモリ割り当てインスペクションを導入する予定です。 これは、 小さなオブジェクトヒープ からの多くの問題が特定の問題タイプを取得することを意味します。 今のところ、 小さなオブジェクトヒープ タイプの問題に、このページで説明されているパターンがあるかどうかを手動で確認することをお勧めします。
デフォルトのしきい値は 100MB です。
ボクシング
ボクシングは、値型を参照型に変換しています。 たとえば、次のようになります。
問題は、値型がスタックに格納されているのに対し、参照型はマネージヒープに格納されていることです。 これはパフォーマンスに 2 度影響します。
文字列に整数値を割り当てるには、CLR がスタックから値を取得してヒープにコピーする必要があります。
マネージヒープに格納されているオブジェクト(この場合は文字列)はガベージコレクションされます。
DPA がこれを示す方法
ボクシングの検出は、DPA を使用すると簡単です。 問題リストで、割り当てられたタイプを確認します。 値型の場合、間違いなくボクシングの結果です。 例: 10000 回以上のコードを呼び出しましょう。 この問題は、割り当てられたタイプが int32 であることを示しています。

エディターでは、ヒープ割り当てビューアーはボクシング割り当てについても警告します。

直す方法
通常、ボクシングを必要としないようにコードを書き直すことができます。 この例では、簡単な修正は ToString() メソッドを呼び出すことです:
構造体の代わりにクラス
ボクシングの別の例は、 struct 型に関連しています。 タイプが単一の値を表す場合は、 struct として定義できます。 これを class として定義すると(これはよくある間違いです)、参照型になります。 そのインスタンスはスタックではなくマネージヒープに配置されるため、不要になったインスタンスはガベージコレクションする必要があります。
例: enum フィールドと int プロパティで構成される Account タイプがあります。 タイプを struct として定義できますが、代わりに誤って class を使用しています。
DPA がこれを示す方法
残念ながら、この問題に関するヒントはありません。 DPA に表示されるのは、コードが参照タイプのインスタンスを割り当てることだけです。 小さなオブジェクトヒープまたは大きなオブジェクトヒープへの「通常の」割り当てにすぎません。 たとえば、10000 アカウントを作成するとします。

エディターでは、これは次のようになります。

直す方法
修正は明確です。 class ではなく struct として型を定義してください。 この例では、次のようになります:
コレクションのサイズ変更
Dictionary、 List、 HashSet、 StringBuilder などの動的サイズのコレクションには次の特徴があります。コレクションサイズが現在の上限を超えると、CLR はコレクションをリサイズし、すべてのコレクションをメモリ内で再定義します。 これが頻繁に起きると、アプリケーションのパフォーマンスに悪影響を及ぼします。
例: リストに要素を追加するコードがあります。
DPA がこれを示す方法
動的コレクションの内部は、マネージヒープ内で、値の型(たとえば、 Dictionary の場合は Int32[] )または String[] 型(List の場合)の配列として見ることができます。 問題のリストでそのような割り当てを見つけることが主なヒントです。

エディターでの主なヒントは、この配列が間接的に割り当てられることです: new キーワードではなく、他の方法による:

行 var list = new List<int>(); は、内部に要素がない空のリストを定義しているため、割り当てを行いません。
直す方法
「サイズ変更」メソッドによって引き起こされるトラフィックが大きい場合、唯一の解決策は、サイズ変更が必要な場合の数を減らすことです。 必要なサイズを予測して、このサイズ以上のコレクションを初期化してください。 別の戦略は、 Truncate() を使用してさらに切り捨てて、保証された十分な量のメモリを割り当てることです。
この例では、修正は次のようになります。
コレクションの列挙
動的コレクションを使用する場合は、列挙する方法に注意してください。 ここでの典型的な主な頭痛の種は、 IEnumerable インターフェースを実装していることだけを知っている foreach を使用してコレクションを列挙することです。 次の例を検討してください。
EnumerateCollection() メソッドのリストは IEnumerable インターフェースへキャストされるため、列挙子のさらなる ボクシングが発生します。
DPA がこれを示す方法
Enumerator<> タイプの割り当ての問題を確認する必要があります。

エディターでは、コレクションを列挙するコードの場所でこれらの割り当てを確認できます。

これは配列にも当てはまることに注意してください。 唯一の違いは、 System.SZGenericArrayEnumerator<> タイプの割り当てについて問題を確認する必要があることです。

直す方法
コレクションをインターフェースにキャストしないでください。 上記の例での最善の解決策は、 List<string> コレクションを受け入れる EnumerateCollection() メソッドオーバーロードを作成することです。
文字列の内容を変更する
String はイミュータブル型であり、文字列オブジェクトの内容を変更できません。 文字列の内容を変更すると、新しい string オブジェクトが生成されます。 このことが、文字列によるパフォーマンス問題の主な原因になっています。 文字列の内容を変更するほど、メモリ割り当ても増えます。 この結果、ガベージコレクションがトリガーされ、アプリのパフォーマンスに影響します。 シンプルな対策は、新しい string オブジェクトの生成を最小限に抑えるようにコードを最適化することです。
文字列を逆にする関数の例を考えてみましょう:
DPA がこれを示す方法
この課題について DPA から追加のヒントはありません。表示されるのは、小または大規模オブジェクトヒープへの割り当てのみです。

直す方法
ほとんどの場合、修正は StringBuilder クラスを使用するか、特定の配列メソッドを使用して文字列を文字の配列として処理することです。 この例では、コードは次のようになります。
ラージオブジェクトヒープ
.NET で適用されるパフォーマンストリックの 1 つは、ガベージコレクターが未使用のオブジェクトを削除するだけでなく、マネージヒープを圧縮する必要があるという事実から来ています。 圧縮は単純なコピーによって行われるため、パフォーマンスがさらに低下します。 調査によると、コピーされたオブジェクトが 85 KB を超える場合、これらのペナルティがヒープ圧縮の利点を上回ります。 このため、そのようなオブジェクトはすべて、ラージオブジェクトヒープ(LOH)と呼ばれるマネージヒープの別のセグメントに配置されます。 LOH 内の生き残ったオブジェクトは圧縮されません(ただし、完全なガベージコレクション中にガベージコレクターに LOH を圧縮させることができます)。 これには 2 つの欠点があります。
アプリケーションは時間経過とともにどんどんメモリを消費します。LOH が断片化します。
パフォーマンスのペナルティ: LOH との相互作用は、Small Object Heap よりも複雑です。
DPA がこれを示す方法
DPA は LOH への割り当てをすべて記録するだけで、その原因を区別しません。 Small Object ヒープへの割り当てと同様に、LOH への割り当てもそれ自体には意味がありません。現在のユースケースで割り当てが必要な場合もあります。
デフォルトのしきい値は 50MB です。
LOH への割り当ては ラージオブジェクトヒープ 問題としてマークされます。

直す方法
LOH への割り当てが避けられない、または本当に必要であることを確認してください。 LOH でオブジェクトを割り当てることは、アプリケーション(キャッシュなど)の全寿命に耐える必要がある大規模なコレクションの場合などに意味があります。