对于下面的程序(确实是在 Release模式下运行,而未连接调试器)感到非常好奇,第一个循环为数组的每个元素分配了一个新对象,大约需要一秒钟才能运行。
所以我想知道哪个部分花费最多的时间-对象创建或分配。因此,我创建了第二个循环来测试创建对象所需的时间,并创建了第三个循环来测试分配时间,它们都在几毫秒内运行。这是怎么回事?
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
}
}
最佳答案
当创建一个占用少于85,000字节RAM且不是double
数组的对象时,该对象将被放置在称为“零代生成”堆的内存区域中。每次Gen0堆增长到一定大小时,系统可以在其中找到实时引用的Gen0堆中的每个对象都将复制到Gen1堆中。然后,Gen0堆将被批量擦除,因此它有空间容纳更多新对象。如果Gen1堆达到某个大小,则将存在引用的所有内容都复制到Gen2堆中,然后可以批量擦除Gen0堆。
如果创建了许多对象并立即将其丢弃,则Gen0堆将反复填充,但是Gen0堆中的对象很少,必须将其复制到Gen1堆中。因此,如果有的话,Gen1堆将非常缓慢地被填充。相反,如果在Gen0堆变满时仍引用了Gen0堆中的大多数对象,则系统将不得不将这些对象复制到Gen1堆中。这将迫使系统花费时间复制这些对象,并且可能还会使Gen1堆填满,以至于必须对其进行扫描以查找 Activity 对象,并且必须将那里的所有 Activity 对象再次复制到Gen2堆中。 。所有这些花费更多时间。
另一个使您的第一个测试变慢的问题是,当尝试识别所有 Activity 的Gen0对象时,系统只有忽略自上一个Gen0集合以来未触及过的任何Gen1或Gen2对象,系统才能忽略它们。在第一个循环中,objects
数组将不断被触摸;因此,每个Gen0集合都必须花费时间来处理它。在第二个循环中,它根本没有被触及,因此,即使有很多Gen0集合,它们也不需要花费很长时间即可执行。在第三个循环中,数组将不断被触及,但不会创建新的堆对象,因此不需要垃圾回收周期,并且占用多长时间也无关紧要。
如果您要添加第四个循环,该循环在每次通过时创建并放弃一个对象,但是还将对先前存在的对象的引用存储到数组插槽中,那么我希望它所花费的时间将比第二个循环的总时间更长。和第三个循环,即使它会执行相同的操作。也许没有第一个循环那么长的时间,因为很少有新创建的对象需要从Gen0堆中复制出来,但是比第二个循环要长,因为要确定仍然存在的对象还需要进行额外的工作。如果您想进一步探究问题,那么使用嵌套循环进行第五次测试可能会很有趣:
for (int ii=0; ii<1024; ii++)
for (int i=ii; i<Count; i+=1024)
..
我不知道确切的细节,但是.NET试图通过将它们分割为多个块,来避免扫描整个只有一小部分被触及的大型数组。如果触摸了大型数组的某个块,则必须扫描该块中的所有引用,但是存储在块中的引用(自上次Gen0集合以来没有被触摸过)可能会被忽略。如上所示破坏循环可能会导致.NET最终接触Gen0集合之间的数组中的大多数块,这很可能会产生比第一个循环慢的时间。
关于c# - C#性能好奇心,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18165019/