38

プロジェクトのコードのレビューを開始したところ、次のようなものが見つかりました。

GC.Collect();
GC.WaitForPendingFinalizers();

これらの行は通常、効率を高めるという理論的根拠の下でオブジェクトを破壊するために考案されたメソッドに表示されます。私はこの発言をしました:

  1. すべてのオブジェクトの破棄時にガベージ コレクションを明示的に呼び出すと、パフォーマンスが低下します。これは、CLR のパフォーマンスに絶対に必要かどうかが考慮されていないためです。
  2. これらの命令をこの順序で呼び出すと、他のオブジェクトがファイナライズされている場合にのみ、すべてのオブジェクトが破棄されます。したがって、独立して破棄される可能性のあるオブジェクトは、別のオブジェクトが実際に必要なく破棄されるのを待つ必要があります。
  3. デッドロックが発生する可能性があります (参照:この質問)

1、2、3 は本当ですか? あなたの答えを裏付ける参考文献を教えてください。

私は自分の発言にほぼ確信を持っていますが、なぜこれが問題なのかをチームに説明するために、自分の主張を明確にする必要があります。それが私が確認と参照を求めている理由です。

4

6 に答える 6

8

非常に理解しやすいポイントがあります。GC を実行すると、実行ごとに多くのオブジェクト (たとえば、10000) が自動的にクリーンアップされます。すべての破壊の後に呼び出すと、実行ごとに約 1 つのオブジェクトがクリーンアップされます。

GC には高いオーバーヘッドがあるため (スレッドを停止および開始する必要があり、すべてのオブジェクトを生きた状態でスキャンする必要があります)、バッチ呼び出しが非常に望ましいです。

また、すべてのオブジェクトの後にクリーンアップすることで、どのようなメリットが得られるでしょうか? これは、バッチ処理よりも効率的である可能性がありますか?

于 2012-09-04T14:41:24.837 に答える
7

ポイント番号 3 は技術的には正しいですが、ファイナライザー中に誰かがロックした場合にのみ発生する可能性があります。

この種の呼び出しがなくても、ファイナライザー内でロックすることは、ここにあるものよりもさらに悪いことです。

GC.Collect()呼び出しが実際にパフォーマンスに役立つ場合がいくつかあります。

これまでのキャリアで 2 回、おそらく 3 回はそうしました。(または、私が行って結果を測定し、再度取り出したものを含めると、おそらく約5〜6回です。これは、実行後に常に測定する必要があるものです)。

短期間に数百または数千メガのメモリを大量に消費し、その後、長期間にわたってあまり集中的でないメモリの使用に切り替える場合、これは大規模または重要な改善になる可能性があります明示的に収集します。それがここで起こっていることですか?

それ以外の場所では、せいぜい遅くなり、より多くのメモリを使用するだけです。

于 2012-09-05T00:10:31.787 に答える
6

ここで私の他の答えを見てください:

GC.Collectするかどうか

GC.Collect() を自分で呼び出すと、次の 2 つのことが発生する可能性があります。コレクションの実行により多くの時間を費やすことになり (手動の GC.Collect() に加えて、通常のバックグラウンド コレクションが引き続き行われるため)、メモリが長くなります(そこに行く必要のない高次の世代にいくつかのものを強制したため)。つまり、GC.Collect() を自分で使用することは、ほとんどの場合、悪い考えです。

GC.Collect() を自分で呼び出したいと思うのは、ガベージ コレクターが知るのが難しいプログラムに関する特定の情報がある場合だけです。標準的な例は、明確なビジー サイクルと軽負荷サイクルを持つ長期実行プログラムです。ビジー サイクルの前に、負荷が軽い期間の終わり近くにコレクションを強制的に実行して、ビジー サイクルのためにリソースができるだけ解放されるようにすることができます。しかし、ここでも、アプリの構築方法を再考することで、より良い結果が得られる場合があります (つまり、スケジュールされたタスクはより適切に機能しますか?)。

于 2012-09-04T14:42:59.340 に答える
6

@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 つのアプローチを取っています。

  1. ジャグ配列対多次元配列に変更します。これにより、必要な連続メモリの量が削減され、理想的には、これらの配列の多くがラージ オブジェクト ヒープから除外されます。
  2. ArrayPool クラスを使用して配列を実装します。
于 2017-11-06T19:28:47.963 に答える
2

Crystal Report ドキュメントのサーバー側キャッシュをクリーンアップするために、これを 1 回だけ使用しました。Crystal Reports 例外で私の応答を参照してください: システム管理者によって構成されたレポート処理ジョブの最大制限に達しました

オブジェクトが適切にクリーンアップされていないことがあったため、WaitForPendingFinalizers は特に役に立ちました。Web ページでのレポートのパフォーマンスが比較的遅いことを考えると、わずかな GC の遅延は無視できる程度であり、メモリ管理の改善により、サーバー全体がより快適になりました。

于 2016-08-24T21:21:32.317 に答える