我已经看到很多关于是否在for
循环范围内部或外部声明变量的问题。我们会详细讨论,例如here,here和here。答案是绝对没有性能差异(相同的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
)
因此,这两者之间存在差异,而这种差异并未被优化,这对我来说是令人惊讶的。
为什么我会看到这一点,这两种方法之间是否存在性能差异?
答案 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变量视为两个,并丢弃循环之间的值。
总结一下:
JIT编译器是#2或#3,因此在IL中使用两个变量是有意义的。
答案 2 :(得分:1)
只是为上面的详细答案添加一些内容。 C#编译器很少进行优化,例如连接字符串文字(&#34; a&#34; +&#34; b&#34;)和计算常量。因此,查看C#编译器生成的IL进行优化是没有意义的。相反,您应该查看JIT编译器生成的汇编程序。
此外,构建参数可以抑制JIT优化。因此,请确保设置发布版本模式并清除&#34;抑制模块加载时的JIT优化&#34; VS调试选项中的标志