@Grzenio と同様の問題に遭遇しましたが、1000x1000 から 3000x3000 のオーダーで、はるかに大きな 2 次元配列を使用しています。これは Web サービスにあります。
メモリを追加することが常に正しい答えであるとは限りません。コードとユース ケースを理解する必要があります。GC 収集なしでは、16 ~ 32 GB のメモリが必要です (顧客の規模によって異なります)。それがなければ、32 ~ 64 GB のメモリが必要になりますが、それでもシステムが影響を受けないという保証はありません。.NET ガベージ コレクターは完璧ではありません。
私たちの Web サービスには、500 万から 5000 万の文字列 (構成に応じてキーと値のペアごとに最大 80 から 140 文字) のメモリ内キャッシュがあります。ブール値は、作業を行うために別のサービスに渡されました。1000x1000 の「マトリックス」(2 次元配列) の場合、これはリクエストごとに約 25 MBです。ブール値は、(キャッシュに基づいて) 必要な要素を示します。各キャッシュ エントリは、「マトリックス」内の 1 つの「セル」を表します。
ページングによりサーバーのメモリ使用率が 80% を超えると、キャッシュのパフォーマンスが大幅に低下します。
私たちが見つけたのは、明示的に GC を実行しない限り、.net ガベージ コレクターは、キャッシュ パフォーマンスが大幅に低下する 90 ~ 95% の範囲になるまで一時的な変数を「クリーンアップ」しないということです。
ダウンストリーム プロセスには長時間 (3 ~ 900 秒) かかることが多いため、GC コレクションのパフォーマンスへの影響はごくわずかでした (1 コレクションあたり 3 ~ 10 秒)。クライアントに応答を返した後で、この収集を開始しました。
最終的に、GC パラメーターを構成可能にしました。また、.net 4.6 にはさらにオプションがあります。これが、使用した .net 4.5 コードです。
if (sinceLastGC.Minutes > Service.g_GCMinutes)
{
Service.g_LastGCTime = DateTime.Now;
var sw = Stopwatch.StartNew();
long memBefore = System.GC.GetTotalMemory(false);
context.Response.Flush();
context.ApplicationInstance.CompleteRequest();
System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
System.GC.WaitForPendingFinalizers();
long memAfter = System.GC.GetTotalMemory(true);
var elapsed = sw.ElapsedMilliseconds;
Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
}
.net 4.6 で使用するために書き直した後、ガベージ コレクションを単純な収集と圧縮収集の 2 つのステップに分割しました。
public static RunGC(GCParameters param = null)
{
lock (GCLock)
{
var theParams = param ?? GCParams;
var sw = Stopwatch.StartNew();
var timestamp = DateTime.Now;
long memBefore = GC.GetTotalMemory(false);
GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
GC.WaitForPendingFinalizers();
//GC.Collect(); // may need to collect dead objects created by the finalizers
var elapsed = sw.ElapsedMilliseconds;
long memAfter = GC.GetTotalMemory(true);
Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
}
}
// https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
public static RunCompactingGC()
{
lock (CompactingGCLock)
{
var sw = Stopwatch.StartNew();
var timestamp = DateTime.Now;
long memBefore = GC.GetTotalMemory(false);
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
var elapsed = sw.ElapsedMilliseconds;
long memAfter = GC.GetTotalMemory(true);
Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
}
}
これを調査するのに多くの時間を費やしたので、これが他の誰かに役立つことを願っています.
[編集] これをフォローアップすると、大規模な行列に関するいくつかの追加の問題が見つかりました。プロセス/サーバーに十分なメモリ (24 GB の空き容量) がある場合でも、メモリの負荷が高くなり、アプリケーションが突然配列を割り当てることができなくなりました。詳細な調査の結果、プロセスには「使用中のメモリ」のほぼ 100% のスタンバイ メモリがあることがわかりました (24GB 使用中、24GB スタンバイ、1GB 空き)。「空き」メモリが 0 になると、スタンバイが空きとして再割り当てされる間、アプリケーションは 10 秒以上一時停止し、リクエストへの応答を開始できます。
私たちの調査によると、これは大きなオブジェクト ヒープの断片化が原因であると思われます。
この懸念に対処するために、私たちは 2 つのアプローチを取っています。
- ジャグ配列対多次元配列に変更します。これにより、必要な連続メモリの量が削減され、理想的には、これらの配列の多くがラージ オブジェクト ヒープから除外されます。
- ArrayPool クラスを使用して配列を実装します。