广泛使用LOH会导致严重的性能问题

时间:2014-12-09 14:55:21

标签: c# performance garbage-collection large-object-heap

我们在Server 2012上使用了WebApi 2,.NET 4.5的Web服务。我们看到偶尔的延迟增加了10-30ms,没有充分的理由。我们能够向LOH和GC追踪有问题的代码。

我们将一些文本转换为其UTF8字节表示(实际上,我们使用的序列化库就是这样)。只要文本短于85000字节,延迟就会稳定且短暂:平均为~0.2 ms,为99%。一旦超过85000边界,平均延迟增加到约1ms,而99%跳跃到16-20ms。 Profiler显示大部分时间都花在了GC上。可以肯定的是,如果我在迭代之间放置GC.Collect,测得的延迟会回到0.2ms。

我有两个问题:

  1. 延迟来自哪里?据我了解LOH 没有压缩。 SOH正在被压缩,但没有显示延迟。
  2. 有解决这个问题的实用方法吗?注意     我无法控制数据的大小并使其变小。
  3. -

    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)]);
    }
    

2 个答案:

答案 0 :(得分:5)

性能问题通常来自两个方面:分配和碎片。

<强>分配

运行时保证清理内存,因此花费周期清理它。当你分配一个大对象时,那就是大量的内存并且开始为单个分配添加毫秒(当说实话,.NET中的简单分配实际上非常快,所以我们通常从不关心这个。) / p>

分配LOH对象然后回收时发生碎片。直到最近,GC无法重新组织内存以移除这些旧对象&#34;间隙&#34;,因此如果它是相同大小或更小,则只能适应该间隙中的下一个对象。最近,GC已经能够压缩LOH,从而消除了这个问题,但在压实过程中需要花费时间。

我的猜测在您的情况下,您遇到了两个问题并触发了GC运行,但这取决于您的代码尝试在LOH中分配项目的频率。如果要进行大量分配,请尝试对象池路由。如果您无法有效地控制池(块状对象生命周期或不同的使用模式),请尝试对您正在使用的数据进行分块以完全避免它。

<小时/> 您的选项

我遇到过两种方法:

  • 避免它。
  • 使用它,但意识到您正在使用它并明确管理它。

避免

这涉及将你的大型物体(通常是某种阵列)分块成每个落在LOH屏障下的块。我们在序列化大对象流时执行此操作。运行良好,但实施将特定于您的环境,所以我对提供编码示例犹豫不决。

使用

解决分配和碎片问题的一种简单方法是长期存在的对象。明确地创建一个大的空数组(或数组)来容纳你的大对象,并且不要去除它(或它们)。保留它并像对象池一样重复使用它。您为此分配付费,但可以在首次使用时或在应用程序空闲时间内执行此操作,但您为重新分配支付的费用较少(因为您没有重新分配)并减少碎片问题,因为您并非如此。不断要求分配东西,你不回收物品(这导致了第一个空白)。

那就是说,中途的房子可能是有序的。为对象池预先保留一段内存。提前完成,这些分配应该在内存中连续,这样您就不会获得任何间隙,并将可用内存的尾端留给不受控制的项目。请注意,这显然会对应用程序的工作集产生影响 - 对象池占用空间而不管它是否被使用。

<小时/> 的资源

LOH在网络上有很多内容,但要注意资源的日期。在最新的.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

答案 1 :(得分:3)

除了以下内容,确保您正在使用server garbage collector。这并不影响LOH的使用方式,但我的经验是它确实显着减少了在GC中花费的时间。

我发现避免大对象堆问题的最佳工作是创建一个持久缓冲区并重新使用它。因此,不是每次调用Encoding.GetBytes时都分配一个新的字节数组,而是将字节数组传递给方法。

在这种情况下,使用带有字节数组的GetBytes overload。分配一个足够大的数组来保存最长的预期字符串的字节,并保持它。例如:

// 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();
        // ...
    }

这里唯一的问题是你必须确保你的缓冲区足够大以容纳最大的字符串。我过去所做的是将缓冲区分配给我期望的最大尺寸,但是每当我去使用它时检查以确保它足够大。如果它不够大,那么重新分配它。你如何做到这一点取决于你想要的严谨程度。在使用主要西欧文本时,我只是将字符串长度加倍。例如:

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添加一些填充,以便分配更多,以防您有其他异常值。