重用for循环迭代变量

时间:2017-04-25 01:22:03

标签: c# .net performance cil

我已经看到很多关于是否在for循环范围内部或外部声明变量的问题。我们会详细讨论,例如hereherehere。答案是绝对没有性能差异(相同的IL),但为了清楚起见,在最严格的范围内声明变量是首选。

我对一个稍微不同的情况感到好奇:

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);
}

我希望两种方法在Release模式下编译为相同的IL。然而,这种情况并非如此。我会给你完整的IL,并指出差异。第一种方法有一个本地:

.locals init (
    [0] int32 i
)

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

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

因此,这两者之间存在差异,而这种差异并未被优化,这对我来说是令人惊讶的。

为什么我会看到这一点,这两种方法之间是否存在性能差异?

3 个答案:

答案 0 :(得分:9)

要回答您的问题,您实际上已在第一种情况下声明了一个局部变量,在第二种情况下实际声明了两个局部变量。 C#编译器显然不会重用局部变量,即使我认为可以这样做。我的猜测是,这并不是一个值得编写复杂分析来处理的性能提升,如果JIT足够智能,无论如何都可能无法处理它。但是,您期望看到的优化已完成,而不是在IL级别。它由JIT编译器在发出的机器代码中完成。

这是一个足够简单的案例,其中检查发出的机器代码实际上是提供信息的。总结一下,这两个方法将JIT编译为相同的机器代码(x86如下所示,但x64机器代码也相同),因此使用较少的局部变量没有性能提升。

关于条件的快速说明,我把这两个片段都放到了不同的方法中。然后我查看Visual Studio 2015中的反汇编,使用.NET 4.6.1运行时,x86发布版本(即优化)并在之后附加调试器 JIT已编译方法(至少在没有附带调试器的调用)。我禁用方法内联以保持两种方法之间的一致性。要查看反汇编,请在所需方法中放置一个断点,附加,转到Debug&gt; Windows&gt;拆卸。按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中进行验证。

答案 1 :(得分:2)

作为对现有答案的补充,请注意,将两个变量合并为一个实际上可能损害性能,具体取决于JIT编译器能够推断出的信息。

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

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

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

总结一下:

  1. 对于不能分析变量生命周期的哑编译器,一个变量优于两个。
  2. 对于一个可以分析变量生命周期而不能分割变量的合适编译器,两个变量优于一个。
  3. 对于智能编译器,它可以分析变量的生命周期,也可以分割变量,这没关系。
  4. JIT编译器是#2或#3,因此在IL中使用两个变量是有意义的。

答案 2 :(得分:1)

只是为上面的详细答案添加一些内容。 C#编译器很少进行优化,例如连接字符串文字(&#34; a&#34; +&#34; b&#34;)和计算常量。因此,查看C#编译器生成的IL进行优化是没有意义的。相反,您应该查看JIT编译器生成的汇编程序。

此外,构建参数可以抑制JIT优化。因此,请确保设置发布版本模式并清除&#34;抑制模块加载时的JIT优化&#34; VS调试选项中的标志