【问题标题】:Extensive use of LOH causes significant performance issue广泛使用 LOH 会导致严重的性能问题
【发布时间】:2015-02-07 12:27:12
【问题描述】:

我们在 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.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)]);
}

【问题讨论】:

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


【解决方案1】:

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

分配

运行时保证干净的内存,因此会花费周期来清理它。当您分配一个大对象时,会占用大量内存并开始为单个分配增加毫秒(老实说,.NET 中的简单分配实际上非常快,因此我们通常从不关心这一点)。

当 LOH 对象被分配然后回收时会发生碎片。直到最近,GC 还无法重新组织内存以消除这些旧对象“间隙”,因此只能将下一个对象放入该间隙中,前提是它的大小相同或更小。最近,GC 被赋予了压缩 LOH 的能力,这消除了这个问题,但在压缩过程中会花费时间。

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


您的选择

我遇到了两种 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

【讨论】:

  • @downvoter 我想知道投反对票的原因。我对 LOH 的理解并不完整,所以我想填补任何空白。请贡献并帮助我改进答案。自从被否决以来,我已经多次改写我的答案,所以希望我已经纠正了这个问题,但我不知道。
  • 感谢 Adam,这两个建议的解决方案的问题是我们正在处理第 3 方创建的对象。其中一个字段是恰好高于阈值的字符串。一旦发生这种情况,整个系统就会变慢。字符串从序列化形式变为字符串,由于自身的业务逻辑而被复制了几次。由于我不控制对象,因此无法将字符串替换为其他内容,这样对 LOH 更友好。
  • 如果我没有找到/收到更好的答案,我会再给它一些时间,并且会接受你的答案。
  • @alon 如果您无法访问大型对象本身,您将无能为力。尝试联系供应商。
【解决方案2】:

除了以下内容之外,确保您使用的是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 中添加一些填充,以便分配更多,以防有其他异常值。

【讨论】:

  • 值得注意的是,在使用缓冲区时,需要忽略缓冲区中对象不需要的部分,因此您的代码进行迭代或任何需要注意的实际大小里面的数据,不能依赖Array.Length
  • @AdamHouldsworth:是的。但这就是 GetBytes 的返回值。 . .
  • 确实,我不是在批评解决方案。我过去曾这样做过,但很容易忘记,当您在该解决方案下工作时,您的小阵列可以放在更大的阵列内。此技术还可用于避免在 LOH 内外分配许多小数组。
  • 感谢您的回答。我意识到我可以重新使用缓冲区来避免 GC。代码示例是该问题的演示。实际情况要复杂得多。这些长字符串可能以不同的形式出现在意想不到的地方(取决于第 3 方代码)。
猜你喜欢
  • 1970-01-01
  • 2010-11-14
  • 1970-01-01
  • 2017-12-10
  • 1970-01-01
  • 1970-01-01
  • 2015-02-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多