Foreach循环和变量初始化


11

这两个版本的代码之间有区别吗?

foreach (var thing in things)
{
    int i = thing.number;
    // code using 'i'
    // pay no attention to the uselessness of 'i'
}

int i;
foreach (var thing in things)
{
    i = thing.number;
    // code using 'i'
}

还是编译器不在乎?当我谈到差异时,我指的是性能和内存使用情况。..或基本上没有任何区别,还是编译后两者最终成为相同的代码?


6
您是否尝试过编译两者并查看字节码输出?

4
@MichaelT我不觉得自己有资格比较字节码输出。.如果发现差异,则不确定是否能够理解其确切含义。
Alternatex

4
如果相同,则不需要资格。

1
@MichaelT尽管您确实需要具备足够的资格,才能对编译器是否可以对其进行优化进行很好的猜测,如果可以,则可以在什么条件下进行优化。
Ben Aaronson

@BenAaronson,这可能需要一个不平凡的例子才能使该功能发痒。

Answers:


22

TL; DR-它们是IL层的等效示例。


DotNetFiddle使它很容易回答,因为它使您可以看到最终的IL。

我使用了稍微不同的循环构造形式,以加快测试速度。我用了:

变体1:

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        int x;
        int i;

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

变体2:

        Console.WriteLine("Hello World");
        int x;

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

在这两种情况下,编译后的IL输出都相同。

.class public auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  2
    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)
    IL_0000:  nop
    IL_0001:  ldstr      "Hello World"
    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000b:  nop
    IL_000c:  ldc.i4.0
    IL_000d:  stloc.0
    IL_000e:  br.s       IL_001f

    IL_0010:  nop
    IL_0011:  ldloc.0
    IL_0012:  stloc.1
    IL_0013:  ldloc.1
    IL_0014:  call       void [mscorlib]System.Console::WriteLine(int32)
    IL_0019:  nop
    IL_001a:  nop
    IL_001b:  ldloc.0
    IL_001c:  ldc.i4.1
    IL_001d:  add
    IL_001e:  stloc.0
    IL_001f:  ldloc.0
    IL_0020:  ldc.i4.2
    IL_0021:  cgt
    IL_0023:  ldc.i4.0
    IL_0024:  ceq
    IL_0026:  stloc.2
    IL_0027:  ldloc.2
    IL_0028:  brtrue.s   IL_0010

    IL_002a:  ret
  } // end of method Program::Main

因此,回答您的问题:编译器优化了变量的声明,并使两个变量等效。

据我了解,.NET IL编译器将所有变量声明移至函数的开头,但我找不到清楚说明2的良好来源。在这个特定的示例中,您看到它通过以下语句使它们向上移动:

    .locals init (int32 V_0,
             int32 V_1,
             bool V_2)

我们在进行比较时过于痴迷...

情况A,所有变量是否都向上移动?

为了进一步探讨这一点,我测试了以下功能:

public static void Main()
{
    Console.WriteLine("Hello World");
    int x=5;

    if (x % 2==0) 
    { 
        int i = x; 
        Console.WriteLine(i); 
    }
    else 
    { 
        string j = x.ToString(); 
        Console.WriteLine(j); 
    } 
}

此处的区别是我们根据比较声明了an int i或a string j。再次,编译器使用以下命令将所有局部变量移至函数2的顶部:

.locals init (int32 V_0,
         int32 V_1,
         string V_2,
         bool V_3)

我发现有趣的是,即使int i在此示例中未声明,也仍在生成支持该代码的代码。

案例B:foreach而不是for呢?

有人指出,这种foreach行为for与行为不同,而且我没有检查被问到的同一件事。因此,我放入了这两部分代码以比较最终的IL。

int 循环外的声明:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};
    int i;

    foreach(var thing in things)
    {
        i = thing;
        Console.WriteLine(i);
    }

int 循环内的声明:

    Console.WriteLine("Hello World");
    List<int> things = new List<int>(){1, 2, 3, 4, 5};

    foreach(var thing in things)
    {
        int i = thing;
        Console.WriteLine(i);
    }

带有foreach循环的最终IL 确实不同于使用该for循环生成的IL 。具体来说,init块和循环部分已更改。

.locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0,
         int32 V_1,
         int32 V_2,
         class [mscorlib]System.Collections.Generic.List`1<int32> V_3,
         valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> V_4,
         bool V_5)
...
.try
{
  IL_0045:  br.s       IL_005a

  IL_0047:  ldloca.s   V_4
  IL_0049:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
  IL_004e:  stloc.1
  IL_004f:  nop
  IL_0050:  ldloc.1
  IL_0051:  stloc.2
  IL_0052:  ldloc.2
  IL_0053:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0058:  nop
  IL_0059:  nop
  IL_005a:  ldloca.s   V_4
  IL_005c:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
  IL_0061:  stloc.s    V_5
  IL_0063:  ldloc.s    V_5
  IL_0065:  brtrue.s   IL_0047

  IL_0067:  leave.s    IL_0078

}  // end .try
finally
{
  IL_0069:  ldloca.s   V_4
  IL_006b:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
  IL_0071:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  IL_0076:  nop
  IL_0077:  endfinally
}  // end handler

foreach方法生成了更多的局部变量,并且需要一些额外的分支。本质上,它第一次跳转到循环的末尾以获得枚举的第一次迭代,然后又跳回到循环的几乎顶部以执行循环代码。然后,它会继续按您的期望循环。

但是,除了使用forforeach构造引起的分支差异之外,基于声明的放置位置,IL中也没有差异int i。因此,我们仍将两种方法等同。

情况C:不同的编译器版本如何?

在剩下的1条评论中,有一个SO问题的链接,该问题与有关使用foreach和使用闭包进行变量访问的警告有关。在这个问题中真正引起我注意的部分是.NET 4.5编译器的工作方式与早期版本的编译器可能存在差异。

那就是DotNetFiddler网站让我失望的地方-他们所能使用的只是.NET 4.5和Roslyn编译器的版本。因此,我调出了Visual Studio的本地实例并开始测试代码。为了确保我比较的是相同的东西,我将.NET 4.5的本地生成的代码与DotNetFiddler代码进行了比较。

我注意到的唯一区别是局部init块和变量声明。本地编译器在命名变量时更加具体。

  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> things,
           [1] int32 thing,
           [2] int32 i,
           [3] class [mscorlib]System.Collections.Generic.List`1<int32> '<>g__initLocal0',
           [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
           [5] bool CS$4$0001)

但是,尽管有微小的差异,但到目前为止,一切都很好。我在DotNetFiddler编译器和本地VS实例生成的内容之间具有等效的IL输出。

因此,我然后重新构建了针对.NET 4,.NET 3.5以及很好的.NET 3.5 Release模式的项目。

在所有这三种其他情况下,生成的IL都是等效的。目标.NET版本对这些样本中生成的IL没有影响。


总结一下这一冒险过程: 我想我们可以自信地说,编译器不在乎声明原始类型的位置,并且使用这两种声明方法对内存或性能都没有影响。不管使用foror foreach循环,这都适用。

我考虑过运行另一种在foreach循环内部合并闭包的情况。但是您曾经询问过在哪里声明原始类型变量的影响,所以我认为我的研究超出了您感兴趣的范围。我前面提到的SO问题有一个很好的答案,它很好地概述了对foreach迭代变量的关闭效果。

1 感谢Andy为foreach循环中的闭包提供了SO问题的原始链接。

2 值得注意的是,ECMA-335规范在I.12.3.2.2节“局部变量和参数”中解决了此问题。我必须查看生成的IL,然后阅读本节以清楚了解发生了什么。感谢棘轮怪胎在聊天中指出这一点。


1
for和foreach的行为不同,问题包括不同的代码,当循环中存在闭包时,这些代码就变得很重要。 stackoverflow.com/questions/14907987/…–
安迪

1
@Andy-感谢您的链接!我继续并使用foreach循环检查了生成的输出,还检查了目标.NET版本。

0

根据您使用的编译器(我什至不知道C#是否有多个),您的代码在被转换为程序之前都会进行优化。一个好的编译器会看到您每次使用不同的值重新初始化相同的变量,并有效地管理它的内存空间。

如果您每次都将同一变量初始化为常量,则编译器将同样在循环之前对其进行初始化并对其进行引用。

这完全取决于编译器的编写水平,但是就编码标准而言,变量应始终具有尽可能小的范围。因此,在循环内部进行声明是我一直以来所学的内容。


3
最后一段是否正确取决于两件事:在您自己的程序的唯一上下文中最小化变量范围的重要性,以及编译器关于是否实际优化多个分配的内在知识。
罗伯特·哈维

然后是运行时,该运行时将字节代码进一步转换为机器语言,其中还执行了许多相同的优化(在此作为编译器优化进行讨论)。
Erik Eidt 2015年

-2

首先,您只是声明并初始化内部循环,因此每次循环循环时,都会在内部循环中重新初始化“ i”。第二秒钟,您仅在循环之外声明。


1
这似乎并没有提供超过2年前发布的最佳答案所提出和解释的要点的实质内容
gnat '18

2
感谢您提供答案,但是并没有提供任何新的方面尚未被接受的,评分最高的答案(没有详细介绍)。
CharonX
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.