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

时间:2016-05-09 16:29:48

标签: 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.");
   }
}

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

在任何一种情况下,代码似乎都在做同样数量的工作。它需要分配/初始化一个字符串,然后为数组位置分配一个引用。唯一的区别是该引用是否是本地var&#34; str&#34;或&#34; str2&#34;。

为什么它会产生如此大的性能差异,将参考分配给&#34; str&#34; vs.&#34; str2&#34;?

如果我们看一下反汇编,就会有所不同:

(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

&#34;慢&#34;版本还有两个&#34; mov&#34;操作所在的&#34;快速&#34;版本只有一个&#34; nop&#34;。

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

2 个答案:

答案 0 :(得分:76)

你认为代码在任何一种情况下的工作量都相同。

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

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

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

请注意,.NET GC倾向于花费时间与活动对象的数量呈线性关系:活动对象需要遍历并移开,而死对象根本不需要任何费用(它们只是被覆盖了下次分配内存)。

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

请查看您的计划的GC performance counters,我怀疑您会在程序之间看到非常不同的结果。

答案 1 :(得分:1)

不,本地参考不会很慢。

什么是缓慢的,是创建大量新的字符串实例,它们是类。快速版本重用相同的实例。这也可以被优化掉,而构造函数调用则不能。