简体   繁体   English

当程序创建大量对象时,C# 应用程序 CPU 性能急剧下降

[英]C# application CPU performance drastically slows down when program creates a high number of objects

I meet this weird performance issue:我遇到了这个奇怪的性能问题:

  1. I have a C# application which creates millions of C# objects.我有一个 C# 应用程序,它创建了数百万个 C# 对象。

  2. In an unrelated part of the code, the application does a specific work which does not depend on the data allocated at step 1.在代码的不相关部分,应用程序执行不依赖于在步骤 1 分配的数据的特定工作。

The CPU times seem to be correlated to the number of objects created at step 1. CPU 时间似乎与在步骤 1 中创建的对象数量相关。

I wrote a simple C# case which reproduces my issue.我写了一个简单的 C# 案例来重现我的问题。 slowdown command is called with the number of millions of string objects created before the DoMyWork() method is called.使用在调用DoMyWork()方法之前创建的数百万个字符串对象来调用slowdown命令。 As you can see, the same DoMyWork() method can take up to 3s if 200M of strings are instantiated.如您所见,如果实例化 200M 的字符串,则相同的DoMyWork()方法最多可能需要 3 秒。

  • Do I miss something in the language ?我是否遗漏了语言中的某些内容?
  • Suppose the physical memory limit is not reached, is there a max number of objects that should not be reached otherwise CLR would slow down ?假设未达到物理内存限制,是否存在不应达到的最大对象数,否则 CLR 会减慢速度?

I ran my test under Windows 10 on Intel Core i7-6700 and my program is a console release built in 32 bits mode (VS 2017 - fw 4.6.1):我在英特尔酷睿 i7-6700 上的 Windows 10 下运行了我的测试,我的程序是一个内置 32 位模式的控制台版本(VS 2017 - fw 4.6.1):

slowdown 0
Allocating 40000 hashtables: 2 ms
Allocating 40000 hashtables: 4 ms
Allocating 40000 hashtables: 15 ms
Allocating 40000 hashtables: 2 ms
Allocating 40000 hashtables: 5 ms
Allocating 40000 hashtables: 5 ms
Allocating 40000 hashtables: 2 ms
Allocating 40000 hashtables: 18 ms
Allocating 40000 hashtables: 10 ms
Allocating 40000 hashtables: 19 ms

slowdown 0 uses ~30M减速 0 使用 ~30M

slowdown 200
Allocating 40000 hashtables: 392 ms
Allocating 40000 hashtables: 1120 ms
Allocating 40000 hashtables: 3067 ms
Allocating 40000 hashtables: 2 ms
Allocating 40000 hashtables: 31 ms
Allocating 40000 hashtables: 418 ms
Allocating 40000 hashtables: 15 ms
Allocating 40000 hashtables: 2 ms
Allocating 40000 hashtables: 18 ms
Allocating 40000 hashtables: 416 ms

slowdown 200 uses ~800M减速 200 次使用 ~800M


using System;
using System.Diagnostics;
using System.Collections;

namespace SlowDown
{
  class Program
  {
    static string[] arr;

    static void CreateHugeStringArray(long size)
    {
      arr = new string[size * 1000000];
      for (int i = 0; i < arr.Length; i++) arr[i] = "";
    }


    static void DoMyWork()
    {
      int n = 40000;
      Console.Write("Allocating " + n + " hashtables: ");
      Hashtable[] aht = new Hashtable[n];

      for (int i = 0; i < n; i++)
      {
        aht[i] = new Hashtable();
      }
    }


    static void Main(string[] args)
    {
      if (0 == args.Length) return;
      CreateHugeStringArray(Convert.ToInt64(args[0]));

      for (int i = 0; i < 10 ; i++)
      {
        Stopwatch sw = Stopwatch.StartNew();
        DoMyWork();
        sw.Stop();
        Console.Write(sw.ElapsedMilliseconds + " ms\n");
      }
    }
  }
}

Likely Garbage Collector nasty stuff, which can freeze you main thread, even if it works mostly on a background thread, as mentionned here : Garbage Collector Thread可能是垃圾收集器讨厌的东西,它可以冻结你的主线程,即使它主要在后台线程上工作,正如这里提到的: 垃圾收集器线程

If you collect it, the time remains (in my case) around 90ms regardless of the size of the "unrelated" array.如果您收集它,无论“无关”数组的大小如何,时间(在我的情况下)都会保持在 90 毫秒左右。

The issue is caused by the Garbage Collector running at the same time as your DoMyWork .该问题是由与DoMyWork同时运行的垃圾收集器引起的。 The sheer size of the array it needs to clean up 'interrupts' the real work.清理“中断”实际工作所需的数组的绝对大小。

To see the impact of the GC, add these lines before your StartNew call - so that the GC work occurs prior to the timing:要查看 GC 的影响,请在您的StartNew调用之前添加这些行 - 以便 GC 工作在计时之前发生:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

The following code creates 10000 new string objects, forcing the garbage collection to run:下面的代码创建了 10000 个新的字符串对象,强制垃圾收集运行:

 string str = "";

 for (int i = 0; i < 10000; i++) str += i;

The performance of the garbage collector is proportional to垃圾收集器的性能与

  • The number of objects which have been allocated已分配的对象数
  • The total amount of memory in use正在使用的内存总量

Your CreateHugeStringArray() allocates very large objects, increasing the total amount of memory in use.您的 CreateHugeStringArray() 分配非常大的对象,增加了正在使用的内存总量。 In extreme cases parts of this memory may be on disk (paged out) further slowing down the system.在极端情况下,该内存的一部分可能会在磁盘上(调出),从而进一步减慢系统速度。

The moral of your story is - don't allocate memory unless you need it.你的故事的寓意是——除非你需要,否则不要分配内存。

Did not find the reason yet, but is seems that having a huge array in LOH slows down garbage collection significantly.还没有找到原因,但似乎在 LOH 中有一个巨大的数组会显着减慢垃圾收集速度。 However, if we create many smaller arrays to hold the same amount of data (which goes to Generation 2 instead of LOH), GC does not slow so much.然而,如果我们创建许多更小的数组来保存相同数量的数据(进入第 2 代而不是 LOH),GC 不会减慢这么多。 It seems that array with 1kk string pointers occupies about 4 million bytes of memory.似乎具有 1kk 字符串指针的数组占用了大约 400 万字节的内存。 So in order to avoid getting to LOH, the array must occupy less than 85 kilobytes.因此,为了避免到达 LOH,阵列必须占用少于 85 KB。 This is about 50 times lesser.这大约少 50 倍。 You may use old trick to split big array into many small arrays您可以使用旧技巧将大数组拆分为许多小数组

    private static string[][] arrayTwoDimentional;

    private static int _arrayLength = 1000000;

    private static int _sizeFromExample = 200;

    static void CreateHugeStringArrayTwoDimentional()
    {
        // Make 50 times more smaller arrays
        arrayTwoDimentional = new string[_sizeFromExample * 50][];

        for (long i = 0; i < arrayTwoDimentional.Length; i++)
        {
            // Make array smaller 50 times
            arrayTwoDimentional[i] = new string[_arrayLength / 50];
            for (var index = 0; index < arrayTwoDimentional[i].Length; index++)
            {
                arrayTwoDimentional[i][index] = "";
            }
        }
    }

    static string GetByIndex(long index)
    {
        var arrayLenght = _arrayLength / 50;
        var firstIndex = index / arrayLenght;
        var secondIndex = index % arrayLenght;

        return arrayTwoDimentional[firstIndex][secondIndex];
    }

Proof that GC is bottleneck here证明 GC 是这里的瓶颈

DoMyWork 内部 After replacing array layout更换阵列布局后

搬家后

In the example, array sizes are hard coded.在示例中,数组大小是硬编码的。 There is a good example on Codeproject how you can calculate the size of type stored object, which will help to adjust the size of arrays: https://www.codeproject.com/Articles/129541/NET-memory-problem-with-uncontrolled-LOH-size-and Codeproject 上有一个很好的例子,如何计算类型存储对象的大小,这将有助于调整数组的大小: https : //www.codeproject.com/Articles/129541/NET-memory-problem-with-不受控制的-LOH-大小-和

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM