对下面的程序非常好奇(是的,在未附带调试器的情况下在发布模式下运行),第一个循环为数组的每个元素分配一个新对象,并且需要大约一秒钟才能运行.
所以我想知道哪个部分占用了最多的时间 - 对象创建或分配.所以我创建了第二个循环来测试创建对象所需的时间,第三个循环来测试分配时间,并且都在几毫秒内运行.这是怎么回事?
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
是的,性能分析低于仅有 10万个对象(1000万将花费太长时间).

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

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

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