16

对下面的程序真的很好奇(是的,在没有附加调试器的情况下在发布模式下运行),第一个循环为数组的每个元素分配一个新对象,并且运行大约需要一秒钟。

所以我想知道哪个部分花费的时间最多——对象创建或分配。因此,我创建了第二个循环来测试创建对象所需的时间,并创建了第三个循环来测试分配时间,两者都在几毫秒内运行。这是怎么回事?

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
    }
}
4

2 回答 2

16

当创建一个占用小于 85,000 字节 RAM 且不是数组的对象时double,它会被放置在称为零代堆的内存区域中。每次 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)
     ..

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

于 2013-08-10T19:27:09.837 回答
14
  1. 您创建1000 万个对象并将它们存储在内存中的不同位置。这里的内存消耗最高。
  2. 您创建了1000 万个对象,但它们没有存储在任何地方, 只是被丢弃了
  3. 您创建1 个对象并对其进行1000 万次引用,从而最大限度地减少内存消耗。

是的,下面的性能分析仅适用于 10000个对象(1000 万个对象需要太长时间)。

仅 10,000 个对象的性能

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

CPU性能案例1

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

CPU性能案例2

UPDATE3:只是为了完整性,第三种情况

CPU性能案例3

于 2013-08-10T18:50:48.853 回答