it-swarm.com.de

Foreach-Schleife und Variableninitialisierung

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?

11
Alternatex

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:

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

Variation 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 äquivalent.

Nach meinem Verständnis verschiebt der .NET IL-Compiler alle Variablendeklarationen an den Anfang der Funktion, aber ich konnte keine gute Quelle finden, die dies eindeutig angibt2. 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 einen int i Oder einen string j Deklarieren. Auch hier verschiebt der Compiler alle lokalen Variablen an den Anfang der Funktion2 mit:

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

Ich fand es interessant festzustellen, dass, obwohl int i In diesem Beispiel nicht deklariert wird, der Code zur Unterstützung immer noch generiert wird.

Fall B: Was ist mit foreach anstelle von for?

Es wurde darauf hingewiesen, dass foreach ein anderes Verhalten hat als for und dass ich nicht dasselbe überprüft habe, worüber 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 Schleife foreach unterschied sich tatsächlich von der IL, die mit der Schleife for generiert 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 foreach -Ansatz 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 oberen Ende der Schleife zurück, um den Schleifencode auszuführen. Es wird dann wie erwartet weiter durchlaufen.

Aber abgesehen von den Verzweigungsunterschieden, die durch die Verwendung der Konstrukte for und foreach verursacht wurden, gab es keinen Unterschied in der IL-Basis wo die int i Deklaration 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 hinterlassen wurde1gab es einen Link zu einer SO-Frage bezüglich einer Warnung zum variablen Zugriff mit foreach und zur 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 im Vergleich zu 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 bemerkte, 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 keine Auswirkungen auf das Gedächtnis oder gibt Leistung mit beiden Deklarationsmethoden. Dies gilt unabhängig von der Verwendung einer for - oder foreach -Schleife.

Ich überlegte, noch einen weiteren Fall auszuführen, der einen Abschluss innerhalb der foreach -Schleife enthielt. Aber Sie hatten nach den Auswirkungen gefragt, bei denen eine Variable vom Typ primitiv deklariert wurde, und ich dachte, ich würde zu weit über das hinausgehen, worüber Sie interessiert waren. Die zuvor erwähnte Frage SO Frage) hat eine gute 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 zu den SO Frage, die Schließungen innerhalb von foreach Schleifen adressiert.

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 für den Hinweis im Chat.

22
user53019

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 kleinstmöglichen Bereich haben. Das Erklären innerhalb der Schleife ist das, was mir immer beigebracht wurde.

0
leylandski