C#表现好奇心

lob*_*ism 16 c# performance

对下面的程序非常好奇(是的,在未附带调试器的情况下在发布模式下运行),第一个循环为数组的每个元素分配一个新对象,并且需要大约一秒钟才能运行.

所以我想知道哪个部分占用了最多的时间 - 对象创建或分配.所以我创建了第二个循环来测试创建对象所需的时间,第三个循环来测试分配时间,并且都在几毫秒内运行.这是怎么回事?

static class Program
{
    const int Count = 10000000;

    static void Main()
    {
        var objects = new object[Count];
        var sw = new Stopwatch();
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~800 ms
        sw.Restart();
        object o = null;
        for (var i = 0; i < Count; i++)
        {
            o = new object();
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 40 ms
        sw.Restart();
        for (var i = 0; i < Count; i++)
        {
            objects[i] = o;
        }
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds); // ~ 50 ms
    }
}
Run Code Online (Sandbox Code Playgroud)

sup*_*cat 15

当创建占用少于85,000字节RAM且不是数组的double对象时,它被放置在称为Generation Zero堆的内存区域中.每当Gen0堆增长到一定大小时,系统可以找到实时引用的Gen0堆中的每个对象都被复制到Gen1堆; 然后批量擦除Gen0堆,以便有更多新对象的空间.如果Gen1堆达到一定大小,那么存在引用的所有内容都将被复制到Gen2堆中,从而可以批量擦除Gen0堆.

如果创建并立即放弃了许多对象,Gen0堆将重复填充,但Gen0堆中的极少数对象必须复制到Gen1堆.因此,如果有的话,Gen1堆将非常缓慢地填充.相反,如果Gen0堆中的大多数对象仍然在Gen0堆已满时被引用,则系统必须将这些对象复制到Gen1堆.这将迫使系统花费时间复制这些对象,并且Gen1堆也可能填满足够的以至于必须扫描实时对象,并且那里的所有活动对象将不得不再次复制到Gen2堆.所有这些都需要更多时间.

在第一次测试中减慢速度的另一个问题是,当尝试识别所有实时Gen0对象时,系统可以忽略任何Gen1或Gen2对象,只有在自上一代Gen0集合以来尚未触及它们时.在第一个循环期间,objects阵列将不断触摸; 因此,每个Gen0集合都必须花时间处理它.在第二个循环期间,它根本没有被触及,所以即使将有尽可能多的Gen0集合,它们也不会花费很长时间来执行.在第三个循环期间,将不断触摸数组,但是不会创建新的堆对象,因此不需要垃圾收集循环,并且它们将花费多长时间并不重要.

如果你要添加第四个循环,它在每个传递中创建并放弃了一个对象,但是也存储在一个数组槽中,引用一个预先存在的对象,我希望它需要的时间比第二个的合并时间长和第三个循环,即使它将执行相同的操作.可能没有第一个循环那么多的时间,因为很少有新创建的对象需要从Gen0堆中复制出来,但是比第二个循环更长,因为需要额外的工作来确定哪些对象仍然存在.如果你想进一步探究一些事情,用嵌套循环进行第五次测试可能会很有趣:

for (int ii=0; ii<1024; ii++)
  for (int i=ii; i<Count; i+=1024)
     ..
Run Code Online (Sandbox Code Playgroud)

我不知道确切的细节,但.NET试图通过将它们细分为块来扫描整个大型数组,其中只有一小部分被触摸.如果触摸了大块的大块,则必须扫描该块中的所有引用,但是可以忽略存储在自上一代Gen0集合以来未被触摸的块中的引用.如上所示断开循环可能会导致.NET最终触及Gen0集合之间数组中的大多数块,很可能比第一个循环产生更慢的时间.


Nen*_*nad 14

  1. 您创建了1000万个对象并将它们存储在内存中的不同位置.这里的内存消耗最高.
  2. 您创建了1000万个对象,但它们不会存储在任何地方, 只是丢弃.
  3. 您创建了1个对象并对其进行了1000万次引用,最小内存消耗.

是的,性能分析低于仅有 10万个对象(1000万将花费太长时间).

只有1万个对象的性能

更新:此图显示了第一种情况下CPU的内存分配工作.注意JIT_New@@...功能占用CPU时间的80.5%.

CPU性能案例1

UPDATE2:以及CaseTwo的完整CPU时间.

CPU性能案例2

更新3:仅为完整性,第三种情况

CPU性能案例3

  • 但这与速度有什么关系呢? (2认同)