对下面的程序非常好奇(是的,在没有附带调试器的情况下在发布模式下运行),第一个循环为数组的每个元素分配一个新对象,并且需要大约一秒钟才能运行。
所以我想知道哪个部分占用了最多的时间 - 对象创建或分配。所以我创建了第二个循环来测试创建对象所需的时间,第三个循环来测试分配时间,并且都在几毫秒内运行。发生了什么事?
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
}
}
答案 0 :(得分: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)
..
我不知道确切的细节,但.NET试图通过将它们细分为块来扫描整个大型数组,其中只有一小部分被触摸。如果触摸了大块的大块,则必须扫描该块中的所有引用,但是可以忽略存储在自上一代Gen0集合以来未被触摸的块中的引用。如上所示断开循环可能会导致.NET最终触及Gen0集合之间数组中的大部分块,很可能比第一个循环产生更慢的时间。
答案 1 :(得分:14)
是的,性能分析低于仅 10万对象(1000万将花费太长时间)。
UPDATE:此图显示了第一种情况下内存分配的CPU工作。注意JIT_New@@...
函数占用80.5%的CPU时间。
UPDATE2:以及CaseTwo的完整CPU时间。
UPDATE3:仅为完整性,第三种情况