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.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

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

分配

运行时保证干净的内存,因此花费周期清理它。当您分配一个大对象时,会占用大量内存并开始为单个分配增加几毫秒(老实说,.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

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

这里唯一的问题是您必须确保缓冲区足够大以容纳最大的字符串。我过去所做的是将缓冲区分配给我期望的最大大小,然后在我使用它时检查以确保它足够大。如果它不够大,则重新分配它。你如何做到这一点取决于你想变得多么严格。在主要处理西欧文本时,我只需将字符串长度加倍。例如:

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 回答