2017-04-25 158 views
4

我已经看到了很多关于是否内部或外部for环路范围内声明变量的问题。这将详细讨论,例如here,herehere。答案是绝对没有性能差异(相同的IL),但为了清楚起见,声明变量在最紧密的范围内是首选。重用的for循环迭代变量

我很好奇,一个稍微不同的情况:

int i; 

for (i = 0; i < 10; i++) { 
    Console.WriteLine(i); 
} 

for (i = 0; i < 10; i++) { 
    Console.WriteLine(i); 
} 

for (int i = 0; i < 10; i++) { 
    Console.WriteLine(i); 
} 

for (int i = 0; i < 10; i++) { 
    Console.WriteLine(i); 
} 

我预计这两种方法来编译相同的IL在Release模式。然而,这种情况并非如此。我会为您提供完整的IL,并指出其差异。第一种方法有一个地方:

.locals init (
    [0] int32 i 
) 

而第二个只有两个当地人,每个for循环计数器:

.locals init (
    [0] int32 i, 
    [1] int32 i 
) 

所以在这两个之间的差异未优化掉,这对我来说很惊奇。

为什么我看到这一点,是有实际的两种方法之间的性能差异?

+1

为什么在意,既然你同意表现也没有什么区别。如何挖掘编译器实现? –

+0

我没有看到任何区别,因为两者都在堆栈 – Slai

+0

@LeiYang链接的问题说没有什么区别,我试图调和我在这个例子中看到的。 – msitt

回答

9

要回答你的问题,你实际上已经宣布第二次在第一种情况下一个局部变量,以及两个。 C#编译器显然不会重用本地变量,即使我认为它可以这样做。我的猜测是,这并不是一个值得编写复杂分析来处理的性能增益,如果JIT足够聪明以处理它,甚至可能不会有用。但是,您期望看到的优化已完成,而不是在IL级别。它由发出的机器代码中的JIT编译器完成。

这是一个很简单的情况下检查所述发射机代码实际上是信息。总结是这两种方法将JIT编译成相同的机器代码(下面显示的x86,但是x64机器代码也是一样的),因此使用较少的本地变量没有性能增益。

的条件快速注意,我带着这两个片段并把它们放到不同的方法。然后,我看着在Visual Studio 2015年拆卸,用.NET运行时4.6.1,X86发布版本(即在优化)和调试器附着的JIT编制方法(至少在调用没有附加调试器)。我禁用了方法内联以保持两种方法之间的一致性。要查看反汇编,请在所需方法中放置一个断点,附加,然后进入Debug> Windows> Disassembly。按F5运行到中断点。

事不宜迟,第一种方法拆解到

  for (i = 0; i < 10; i++) 
010204A2 in   al,dx 
010204A3 push  esi 
010204A4 xor   esi,esi 
      { 
       Console.WriteLine(i); 
010204A6 mov   ecx,esi 
010204A8 call  71686C0C 
      for (i = 0; i < 10; i++) 
010204AD inc   esi 
010204AE cmp   esi,0Ah 
010204B1 jl   010204A6 
      } 

      for (i = 0; i < 10; i++) 
010204B3 xor   esi,esi 
      { 
       Console.WriteLine(i); 
010204B5 mov   ecx,esi 
010204B7 call  71686C0C 
      for (i = 0; i < 10; i++) 
010204BC inc   esi 
010204BD cmp   esi,0Ah 
010204C0 jl   010204B5 
010204C2 pop   esi 
010204C3 pop   ebp 
010204C4 ret 

第二种方法从适当的跳跃不同偏移拆卸到

  for (int i = 0; i < 10; i++) 
010204DA in   al,dx 
010204DB push  esi 
010204DC xor   esi,esi 
      { 
       Console.WriteLine(i); 
010204DE mov   ecx,esi 
010204E0 call  71686C0C 
      for (int i = 0; i < 10; i++) 
010204E5 inc   esi 
010204E6 cmp   esi,0Ah 
010204E9 jl   010204DE 
      } 

      for (int i = 0; i < 10; i++) 
010204EB xor   esi,esi 
      { 
       Console.WriteLine(i); 
010204ED mov   ecx,esi 
010204EF call  71686C0C 
      for (int i = 0; i < 10; i++) 
010204F4 inc   esi 
010204F5 cmp   esi,0Ah 
010204F8 jl   010204ED 
010204FA pop   esi 
010204FB pop   ebp 
010204FC ret 

正如可以看到,在旁边,代码是相同的。

这些方法非常简单,因此跟踪循环计数器的工作是通过esi寄存器完成的。

这是作为练习读者在x64验证。

+2

无论如何,JIT必须进行生命周期分析以协助GC,所以似乎并不合理的是,具有非重叠生命周期的崩溃变量是留给它做的。 –

+0

@Damien_The_Unbeliever是有道理的。 –

1

只是在上面的详细答案中添加了一些内容。 C#编译器几乎不做优化,如连接字符串文字(“a”+“b”)和计算常量。因此,查看由C#编译器生成的用于优化的IL是没有意义的。相反,您应该看看由JIT编译器生成的汇编程序。

此外,生成参数可以抑制JIT优化。因此,请确保您设置了发布构建模式并清除了VS调试选项中的“禁止模块加载时的JIT优化”标志

2

作为对现有答案的补充,请注意将两个变量合并为一个实际上可能会造成受伤性能,这取决于JIT编译器能够推断的信息。

如果JIT编译器看到两个变量具有非重叠生存期,则为免费为两者使用相同的位置(通常是寄存器)。但是如果JIT编译器看到一个变量,则需要才能使用来使用相同的位置。或者更准确地说,它需要在整个生命周期中保持变量的值。

在您的具体情况下,这意味着在第一个循环结束之后且在第二个循环开始之前,编译器不能丢弃该变量的值并将该位置用于其他目的。

但即使使用单个IL变量,也没有给出JIT编译器实际将其视为单个变量的情况。一个智能编译器可以看到,当代码离开第一个循环时,该变量在被覆盖之前不会被再次读取。所以它可以将单个IL变量视为两个,并丢弃循环之间的值。

综上所述:

  1. 对于一个愚蠢的编译器,不分析可变寿命,一个变量是两个以上更好。
  2. 对于体面的编译器来说,它可以分析变量的生命期但不能分割变量,两个变量都比一个好。
  3. 对于一个智能编译器来说,它可以分析变量的生命周期并且还可以分割变量,这并不重要。

JIT编译器是#2或#3,因此在IL中使用两个变量是有意义的。