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

Jon*_*onB 51 c# performance

考虑以下简单程序:

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.");
   }
}
Run Code Online (Sandbox Code Playgroud)

如果我运行它,它的速度相对较快.如果我取消注释"慢"线并注释掉"快速"线,它的速度会慢5倍.请注意,在这两种情况下,它都会在循环内初始化字符串"str2".在任何一种情况下都没有优化(这可以通过查看IL或反汇编来验证).

在任何一种情况下,代码似乎都在做同样数量的工作.它需要分配/初始化一个字符串,然后为数组位置分配一个引用.唯一的区别是该引用是否是本地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
Run Code Online (Sandbox Code Playgroud)

"慢"版本有两个额外的"mov"操作,其中"快速"版本只有一个"nop".

谁能解释一下这里发生了什么?很难看出两个额外的mov操作如何导致> 5x减速,特别是因为我预计大量的时间应该花在字符串初始化上.感谢您的任何见解.

Dan*_*iel 76

你是对的,在任何一种情况下代码的工作量相同.

但垃圾收集器在这两种情况下最终会做出截然不同的事情.

在该str版本中,在给定时间最多有两个字符串实例处于活动状态.这意味着(几乎)第0代中的所有新对象都死亡,没有什么需要升级到第1代.由于第1代根本没有增长,因此GC没有理由尝试昂贵的"完整集合".

在该str2版本中,所有新的字符串实例都是活动的.对象被提升为更高代(可能涉及将它们移动到内存中).此外,由于现在更高的世代正在增长,GC偶尔会尝试运行完整的集合.

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

这意味着str垃圾收集器性能最佳; 虽然str2是最糟糕的情况.

看看你的程序的GC性能计数器,我怀疑你会发现程序之间的结果差别很大.

  • 唯一正确的答案.快速版本中的gen#0集合非常便宜,可以防止内存使用膨胀.抖动优化器无法以其他方式阻止创建不必要的字符串实例,因为代码存在于CLR中,所以无法看到副作用. (3认同)
  • @Jack令人遗憾的是,JIT显然不够聪明,无法优化不必要的字符串构造函数.考虑到这种情况有多简单,这真的令人失望:(有了良好的编译器*应该*在性能方面有显着差异. (2认同)