6

Server 2012 で WebApi 2、.NET 4.5 を使用する Web サービスがあります。正当な理由もなく、時折 10 ~ 30 ミリ秒のレイテンシの増加が見られました。問題のあるコード部分を LOH と GC まで突き止めることができました。

UTF8 バイト表現に変換するテキストがいくつかあります (実際、使用するシリアライゼーション ライブラリはそれを行います)。テキストが 85000 バイトより短い限り、待ち時間は安定して短く、平均で 0.2 ミリ秒以下、99% です。85000 の境界を超えるとすぐに、平均レイテンシーは最大 1 ミリ秒に増加し、99% は 16 ~ 20 ミリ秒に跳ね上がります。Profiler は、ほとんどの時間が GC に費やされていることを示しています。確かに、反復間に GC.Collect を配置すると、測定されたレイテンシは 0.2ms に戻ります。

2 つの質問があります。

  1. レイテンシーはどこから来るのか? 私の知る限り、LOH は圧縮されていません。SOH は圧縮されていますが、待ち時間は表示されません。
  2. これを回避する実用的な方法はありますか?データのサイズを制御して小さくすることはできないことに注意してください。

--

public void PerfTestMeasureGetBytes()
{
    var text = File.ReadAllText(@"C:\Temp\ContactsModelsInferences.txt");
    var smallText = text.Substring(0, 85000 + 100);
    int count = 1000;
    List<double> latencies = new List<double>(count);
    for (int i = 0; i < count; i++)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        var bytes = Encoding.UTF8.GetBytes(smallText);
        sw.Stop();
        latencies.Add(sw.Elapsed.TotalMilliseconds);

        //GC.Collect(2, GCCollectionMode.Default, true);
    }

    latencies.Sort();
    Console.WriteLine("Average: {0}", latencies.Average());
    Console.WriteLine("99%: {0}", latencies[(int)(latencies.Count * 0.99)]);
}
4

2 に答える 2

7

パフォーマンスの問題は通常、割り当てと断片化の 2 つの領域から生じます。

割り当て

ランタイムはクリーン メモリを保証するため、クリーニング サイクルに費やされます。大きなオブジェクトを割り当てると、大量のメモリが割り当てられ、1 つの割り当てに数ミリ秒が追加されます (正直なところ、.NET での単純な割り当ては実際には非常に高速であるため、通常はこれを気にすることはありません)。

断片化は、LOH オブジェクトが割り当てられてから再利用されるときに発生します。最近まで、GC はメモリを再編成してこれらの古いオブジェクトの「ギャップ」を削除することができなかったため、次のオブジェクトが同じサイズかそれより小さい場合にのみ、そのギャップに次のオブジェクトを収めることができました。最近、GC に LOH を圧縮する機能が追加されました。これにより、この問題は解消されますが、圧縮に時間がかかります。

あなたの場合、問題と GC の実行の両方に苦しんでいると思いますが、コードが LOH でアイテムを割り当てようとしている頻度によって異なります多数の割り当てを行っている場合は、オブジェクト プーリング ルートを試してください。プールを効果的に制御できない場合 (オブジェクトの存続期間がゴツゴツしている、または使用パターンがばらばらである) 場合は、作業対象のデータをチャンク化して、完全に回避してください。


あなたのオプション

LOH への 2 つのアプローチに遭遇しました。

  • 避けてください。
  • それを使用しますが、それを使用していることを認識し、明示的に管理してください。

避ける

これには、大きなオブジェクト (通常は何らかの配列) を、それぞれが LOH バリアの下にあるチャンクにチャンクすることが含まれます。これは、大きなオブジェクト ストリームをシリアル化するときに行います。うまく機能しますが、実装は環境に固有のものになるため、コード例を提供することをためらっています.

これを使って

割り当てと断片化の両方に取り組む簡単な方法は、長寿命オブジェクトです。大きなオブジェクトに対応するために大きなサイズの空の配列 (または配列) を明示的に作成し、それ (またはそれら) を取り除かないでください。そのままにして、オブジェクト プールのように再利用します。この割り当てには料金がかかりますが、最初の使用時またはアプリケーションのアイドル時間中にこれを行うことができますが、再割り当ての支払いは少なくなり (再割り当てを行わないため)、フラグメンテーションの問題が軽減されます。ものを割り当てても、アイテムを回収していません(そもそもギャップが発生します)。

とはいえ、中途半端な家が整っているかもしれません。オブジェクト プール用に事前にメモリのセクションを予約します。早い段階でこれらの割り当てをメモリ内で連続させて、ギャップが発生しないようにし、制御されていないアイテムのために使用可能なメモリの最後尾を残す必要があります。ただし、これは明らかにアプリケーションのワーキング セットに影響を与えることに注意してください。オブジェクト プールは、使用されているかどうかに関係なくスペースを占有します。


資力

LOH は Web で多く取り上げられていますが、リソースの日付に注意してください。最新の .NET バージョンでは、LOH が愛され、改善されています。そうは言っても、古いバージョンを使用している場合、LOH は開始から .NET 4.5 (ish) までの長い間、深刻な更新をまったく受け取っていないため、ネット上のリソースはかなり正確であると思います。

たとえば、2008 年のこの記事がありますhttp://msdn.microsoft.com/en-us/magazine/cc534993.aspx

.NET 4.5 の改善点の概要: http://blogs.msdn.com/b/dotnet/archive/2011/10/04/large-object-heap-improvements-in-net-4-5.aspx

于 2014-12-09T15:11:15.913 に答える
3

以下に加えて、サーバーのガベージ コレクターを使用していることを確認してください。これは LOH の使用方法には影響しませんが、私の経験では、GC に費やされる時間が大幅に短縮されます。

大きなオブジェクト ヒープの問題を回避するために私が見つけた最善の回避策は、永続的なバッファーを作成して再利用することです。したがって、 を呼び出すたびに新しいバイト配列を割り当てるのではなくEncoding.GetBytes、バイト配列をメソッドに渡します。

この場合、バイト配列を受け取る GetBytes オーバーロードを使用します。予想される最長の文字列のバイトを保持するのに十分な大きさの配列を割り当て、それを保持します。例えば:

// allocate buffer at class scope
private byte[] _theBuffer = new byte[1024*1024];

public void PerfTestMeasureGetBytes()
{
    // ...
    for (...)
    {
        var sw = Stopwatch.StartNew();
        var numberOfBytes = Encoding.UTF8.GetBytes(smallText, 0, smallText.Length, _theBuffer, 0);
        sw.Stop();
        // ...
    }

ここでの唯一の問題は、バッファーが最大の文字列を保持するのに十分な大きさであることを確認する必要があることです。私が過去に行ったことは、バッファーを予想される最大サイズに割り当てることですが、使用するたびに十分な大きさであることを確認してください。十分な大きさでない場合は、再割り当てします。それをどのように行うかは、どの程度厳密になりたいかによって異なります。主に西ヨーロッパのテキストを扱うときは、文字列の長さを 2 倍にします。例えば:

string textToConvert = ...
if (_theBuffer.Length < 2*textToConvert.Length)
{
    // reallocate the buffer
    _theBuffer = new byte[2*textToConvert.Length];
}

それを行う別の方法は、 を試してGetString、失敗した場合に再割り当てすることです。その後、再試行してください。例えば:

while (!good)
{
    try
    {
        numberOfBytes = Encoding.UTF8.GetString(theString, ....);
        good = true;
    }
    catch (ArgumentException)
    {
        // buffer isn't big enough. Find out how much I really need
        var bytesNeeded = Encoding.UTF8.GetByteCount(theString);
        // and reallocate the buffer
        _theBuffer = new byte[bytesNeeded];
    }
}

バッファーの初期サイズを、予想される最大の文字列に対応できる大きさにすると、おそらくその例外はあまり発生しなくなります。つまり、バッファを再割り当てする必要がある回数は非常に少なくなります。もちろん、他の外れ値がある場合に備えて、 にパディングを追加して、より多くbytesNeededのを割り当てることもできます。

于 2014-12-09T16:00:47.287 に答える