为什么本地var引用导致性能下降很大?

考虑以下简单的程序:

using System; using System.Diagnostics; class Program { private static void Main(string[] args) { const int size = 10000000; var array = new string[size]; var str = new string('a', 100); var sw = Stopwatch.StartNew(); for (int i = 0; i < size; i++) { var str2 = new string('a', 100); //array[i] = str2; // This is slow array[i] = str; // This is fast } sw.Stop(); Console.WriteLine("Took " + sw.ElapsedMilliseconds + "ms."); } } 

如果我运行这个,速度相对较快。 如果我取消注释“缓慢”的行并注释掉“快速”的行,那么速度就会慢5倍以上。 请注意,在这两种情况下,它都会初始化循环内部的string“str2”。 在这两种情况下都没有优化(这可以通过查看IL或反汇编来validation)。

在这两种情况下,代码似乎都在做相同数量的工作。 它需要分配/初始化一个string,然后分配一个数组位置的引用。 唯一的区别是该引用是否是本地var“str”或“str2”。

为什么它将这个引用分配给“str”和“str2”这么大的性能差异呢?

如果我们看看反汇编,有一个区别:

 (fast) var str2 = new string('a', 100); 0000008e mov r8d,64h 00000094 mov dx,61h 00000098 xor ecx,ecx 0000009a call 000000005E393928 0000009f mov qword ptr [rsp+58h],rax 000000a4 nop (slow) var str2 = new string('a', 100); 00000085 mov r8d,64h 0000008b mov dx,61h 0000008f xor ecx,ecx 00000091 call 000000005E383838 00000096 mov qword ptr [rsp+58h],rax 0000009b mov rax,qword ptr [rsp+58h] 000000a0 mov qword ptr [rsp+38h],rax 

“慢”版本有两个额外的“mov”操作,其中“快”版本只有一个“nop”。

任何人都可以解释这里发生了什么? 很难看到两个额外的mov操作如何导致> 5倍的减速,特别是因为我预计大部分时间应该花在string初始化上。 感谢您的任何见解。

你是对的,在这两种情况下,代码的工作量都是相同的。

但垃圾收集器在这两种情况下做的事情截然不同。

str版本中,在给定时间至多有两个string实例处于活动状态。 这意味着(几乎)第0代的所有新对象都死亡,没有什么需要提升到第1代。由于第1代根本没有增长,GC没有理由尝试昂贵的“完整收集”。

str2版本中,所有新的string实例都处于活动状态。 对象被提升到更高的世代(这可能涉及将它们移动到内存中)。 此外,由于更高的世代现在正在增长,GC有时会尝试运行完整的收集。

请注意,.NET GC往往需要时间与活动对象的数量成线性关系:活动对象需要遍历和移动,而死对象根本不需要任何成本(它们只是在下次内存时被覆盖被分配)。

这意味着str是垃圾回收器性能的最佳情况。 而str2是最糟糕的情况。

看看你的程序的GC性能计数器 ,我想你会看到程序之间的结果非常不同。

不,当地的参考不是很慢。

什么是慢,正在创造大量新的string实例,这是类。 而快速版本重用相同的实例。 这也可以优化,而构造函数调用不能。