Foreach-Schleife und variable Initialisierung

11

Gibt es einen Unterschied zwischen diesen beiden Codeversionen?

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'
}

Oder ist es dem Compiler egal? Wenn ich von Unterschieden spreche, meine ich in Bezug auf Leistung und Speichernutzung. ..Oder im Grunde nur ein Unterschied oder sind die beiden nach der Kompilierung der gleiche Code?

Alternatex
quelle
6
Haben Sie versucht, die beiden zu kompilieren und die Bytecode-Ausgabe zu betrachten?
4
@MichaelT Ich habe nicht das Gefühl, qualifiziert zu sein, um die Bytecode-Ausgabe zu vergleichen. Wenn ich einen Unterschied finde, bin ich mir nicht sicher, ob ich verstehen kann, was das genau bedeutet.
Alternatex
4
Wenn es dasselbe ist, müssen Sie nicht qualifiziert sein.
1
@MichaelT Obwohl Sie qualifiziert genug sein müssen, um eine gute Vermutung darüber anzustellen, ob der Compiler es hätte optimieren können, und wenn ja, unter welchen Bedingungen er diese Optimierung durchführen kann.
Ben Aaronson
@BenAaronson und das erfordert wahrscheinlich ein nicht triviales Beispiel, um diese Funktionalität zu kitzeln.

Antworten:

22

TL; DR - das sind äquivalente Beispiele auf der IL-Ebene.


DotNetFiddle macht dies hübsch zu beantworten, da Sie die resultierende IL sehen können.

Ich habe eine etwas andere Variante Ihres Schleifenkonstrukts verwendet, um meine Tests zu beschleunigen. Ich benutzte:

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

Variante 2:

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

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

In beiden Fällen wurde die kompilierte IL-Ausgabe gleich wiedergegeben.

.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

Um Ihre Frage zu beantworten: Der Compiler optimiert die Deklaration der Variablen und macht die beiden Variationen gleich.

Nach meinem Verständnis verschiebt der .NET IL-Compiler alle Variablendeklarationen an den Anfang der Funktion, aber ich konnte keine gute Quelle finden, aus der eindeutig hervorgeht, dass 2 . In diesem Beispiel sehen Sie, dass sie mit dieser Anweisung nach oben verschoben wurden:

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

Wobei wir ein bisschen zu besessen sind, wenn wir Vergleiche anstellen ...

Fall A: Werden alle Variablen nach oben verschoben?

Um dies etwas näher zu untersuchen, habe ich die folgende Funktion getestet:

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

Der Unterschied besteht darin, dass wir basierend auf dem Vergleich entweder ein int ioder ein deklarieren string j. Wieder verschiebt der Compiler alle lokalen Variablen an den Anfang der Funktion 2 mit:

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

Ich fand es interessant festzustellen, dass int ider Code, der ihn unterstützt, immer noch generiert wird , obwohl er in diesem Beispiel nicht deklariert wird.

Fall B: Was ist mit foreachstatt for?

Es wurde darauf hingewiesen, dass sich foreachdas anders verhält als forund dass ich nicht das Gleiche überprüfe, nach dem gefragt wurde. Also habe ich diese beiden Codeabschnitte eingefügt, um die resultierende IL zu vergleichen.

int Deklaration außerhalb der Schleife:

    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 Deklaration innerhalb der Schleife:

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

Die resultierende IL mit der foreachSchleife unterschied sich tatsächlich von der IL, die unter Verwendung der forSchleife erzeugt wurde. Insbesondere wurden der Init-Block und der Schleifenabschnitt geändert.

.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

Der foreachAnsatz erzeugte mehr lokale Variablen und erforderte einige zusätzliche Verzweigungen. Im Wesentlichen springt es beim ersten Mal zum Ende der Schleife, um die erste Iteration der Aufzählung zu erhalten, und springt dann fast zum Anfang der Schleife zurück, um den Schleifencode auszuführen. Es wird dann wie erwartet weiter durchlaufen.

Abgesehen von den Verzweigungsunterschieden, die durch die Verwendung der Konstrukte forund verursacht wurden foreach, gab es keinen Unterschied in der IL, basierend darauf, wo die int iDeklaration platziert wurde. Wir sind also immer noch der Meinung, dass beide Ansätze gleichwertig sind.

Fall C: Was ist mit verschiedenen Compilerversionen?

In einem Kommentar, der 1 hinterlassen wurde , gab es einen Link zu einer SO-Frage bezüglich einer Warnung über den variablen Zugriff mit foreach und die Verwendung von Closure . Der Teil, der mir bei dieser Frage wirklich aufgefallen ist, war, dass es möglicherweise Unterschiede in der Funktionsweise des .NET 4.5-Compilers gegenüber früheren Versionen des Compilers gab.

Und hier hat mich die DotNetFiddler-Site im Stich gelassen - alles, was sie zur Verfügung hatten, war .NET 4.5 und eine Version des Roslyn-Compilers. Also habe ich eine lokale Instanz von Visual Studio aufgerufen und angefangen, den Code zu testen. Um sicherzustellen, dass ich dieselben Dinge verglichen habe, habe ich lokal erstellten Code in .NET 4.5 mit dem DotNetFiddler-Code verglichen.

Der einzige Unterschied, den ich feststellte, war der lokale Init-Block und die Variablendeklaration. Der lokale Compiler war bei der Benennung der Variablen etwas spezifischer.

  .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)

Aber mit diesem kleinen Unterschied war es so weit, so gut. Ich hatte eine äquivalente IL-Ausgabe zwischen dem DotNetFiddler-Compiler und dem, was meine lokale VS-Instanz produzierte.

Also habe ich das Projekt für .NET 4, .NET 3.5 und zum guten Teil für den .NET 3.5 Release-Modus neu erstellt.

In allen drei zusätzlichen Fällen war die generierte IL gleichwertig. Die anvisierte .NET-Version hatte keine Auswirkung auf die IL, die in diesen Beispielen generiert wurde.


Um dieses Abenteuer zusammenzufassen: Ich denke, wir können mit Sicherheit sagen, dass es dem Compiler egal ist, wo Sie den primitiven Typ deklarieren, und dass es bei beiden Deklarationsmethoden keine Auswirkungen auf den Speicher oder die Leistung gibt. Und das gilt unabhängig von der Verwendung einer foroder foreach-Schleife.

Ich überlegte, noch einen weiteren Fall auszuführen, der einen Verschluss innerhalb der foreachSchleife enthielt . Aber Sie hatten nach den Auswirkungen der Deklaration einer primitiven Typvariablen gefragt, und ich dachte, ich würde zu weit über das hinausgehen, worüber Sie interessiert waren. Die zuvor erwähnte SO-Frage hat eine großartige Antwort , die einen guten Überblick über die Schließungseffekte für jede Iterationsvariable bietet.

1 Vielen Dank an Andy für die Bereitstellung des ursprünglichen Links zur SO-Frage zu Schließungen innerhalb von foreachSchleifen.

2 Es ist erwähnenswert, dass die ECMA-335-Spezifikation dies mit Abschnitt I.12.3.2.2 'Lokale Variablen und Argumente' behandelt. Ich musste die resultierende IL sehen und dann den Abschnitt lesen, damit klar wurde, was los war. Vielen Dank an den Ratschenfreak, der im Chat darauf hingewiesen hat.

Gemeinschaft
quelle
1
For und foreach verhalten sich nicht gleich, und die Frage enthält einen anderen Code, der wichtig wird, wenn die Schleife geschlossen wird. stackoverflow.com/questions/14907987/…
Andy
1
@Andy - danke für den Link! Ich habe die generierte Ausgabe mithilfe einer foreachSchleife überprüft und auch die Zielversion von .NET überprüft.
0

Je nachdem, welchen Compiler Sie verwenden (ich weiß nicht einmal, ob C # mehr als einen hat), wird Ihr Code optimiert, bevor er in ein Programm umgewandelt wird. Ein guter Compiler wird feststellen, dass Sie dieselbe Variable jedes Mal mit einem anderen Wert neu initialisieren und den Speicherplatz dafür effizient verwalten.

Wenn Sie dieselbe Variable jedes Mal mit einer Konstanten initialisieren würden, würde der Compiler sie ebenfalls vor der Schleife initialisieren und darauf verweisen.

Es hängt alles davon ab, wie gut Ihr Compiler geschrieben ist, aber was Codierungsstandards betrifft, sollten Variablen immer den geringstmöglichen Umfang haben. Innerhalb der Schleife zu deklarieren ist das, was mir immer beigebracht wurde.

Leylandski
quelle
3
Ob Ihr letzter Absatz wahr ist oder nicht, hängt von zwei Dingen ab: der Wichtigkeit, den Umfang der Variablen im eindeutigen Kontext Ihres eigenen Programms zu minimieren, und dem Wissen des Compilers darüber, ob er tatsächlich die Mehrfachzuweisungen optimiert oder nicht.
Robert Harvey
Und dann gibt es die Laufzeit, die den Bytecode weiter in die Maschinensprache übersetzt, wo viele dieser Optimierungen (hier als Compiler-Optimierungen diskutiert) auch durchgeführt werden.
Erik Eidt
-2

Im ersten Fall deklarieren und initialisieren Sie nur die innere Schleife, sodass jedes Mal, wenn die Schleife eine Schleife erhält, "i" innerhalb der Schleife neu initialisiert wird. In der zweiten deklarieren Sie nur außerhalb der Schleife.

user304046
quelle
1
Dies scheint nichts Wesentliches über Punkte zu bieten, die in der Top-Antwort gemacht und erklärt wurden, die vor über 2 Jahren veröffentlicht wurde
Mücke
2
Vielen Dank, dass Sie eine Antwort gegeben haben, aber es gibt keine neuen Aspekte, die die akzeptierte, am besten bewertete Antwort noch nicht (im Detail) abdeckt.
CharonX