Ciclo foreach e inizializzazione variabile


11

C'è una differenza tra queste due versioni di codice?

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

O al compilatore non importa? Quando parlo di differenza intendo in termini di prestazioni e utilizzo della memoria. ... O praticamente solo qualche differenza o i due finiscono per essere lo stesso codice dopo la compilazione?


6
Hai provato a compilare i due e guardare l'output del bytecode?

4
@MichaelT Non mi sento qualificato per confrontare l'output del bytecode .. Se trovo una differenza, non sono sicuro di riuscire a capire cosa significhi esattamente.
Alternatex,

4
Se è lo stesso, non è necessario essere qualificati.

1
@MichaelT Sebbene tu abbia bisogno di essere sufficientemente qualificato per fare una buona ipotesi sul fatto che il compilatore avrebbe potuto ottimizzarlo e, in tal caso, a quali condizioni è in grado di fare tale ottimizzazione.
Ben Aaronson,

@BenAaronson e che probabilmente richiede un esempio non banale per solleticare quella funzionalità.

Risposte:


22

TL; DR : sono esempi equivalenti a livello di IL.


DotNetFiddle rende questo abbastanza rispondere in quanto consente di vedere l'IL risultante.

Ho usato una variante leggermente diversa del costrutto del tuo loop per rendere più veloci i miei test. Ero solito:

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

Variazione 2:

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

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

In entrambi i casi, l'output IL compilato ha reso lo stesso.

.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

Quindi, per rispondere alla tua domanda: il compilatore ottimizza la dichiarazione della variabile e rende equivalenti le due varianti.

Per quanto ne so, il compilatore .NET IL sposta tutte le dichiarazioni variabili all'inizio della funzione ma non sono riuscito a trovare una buona fonte che affermasse chiaramente che 2 . In questo esempio particolare, vedi che li ha spostati verso l'alto con questa affermazione:

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

In cui diventiamo un po 'troppo ossessivi nel fare confronti ....

Caso A, tutte le variabili vengono spostate verso l'alto?

Per approfondire ulteriormente questo aspetto, ho testato la seguente funzione:

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

La differenza qui è che dichiariamo uno int io uno string jbasato sul confronto. Ancora una volta, il compilatore sposta tutte le variabili locali nella parte superiore della funzione 2 con:

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

Ho trovato interessante notare che anche se int inon verrà dichiarato in questo esempio, il codice per supportarlo viene comunque generato.

Caso B: che dire foreachinvece di for?

È stato sottolineato che foreachha un comportamento diverso rispetto a forquello e che non stavo verificando la stessa cosa che mi era stata chiesta. Quindi ho inserito queste due sezioni di codice per confrontare l'IL risultante.

int dichiarazione al di fuori del ciclo:

    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 dichiarazione all'interno del ciclo:

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

L'IL risultante con il foreachloop era effettivamente diverso dall'IL generato utilizzando il forloop. In particolare, il blocco init e la sezione loop sono cambiati.

.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

L' foreachapproccio ha generato più variabili locali e ha richiesto alcune ramificazioni aggiuntive. In sostanza, la prima volta passa alla fine del ciclo per ottenere la prima iterazione dell'enumerazione, quindi torna alla fine del ciclo per eseguire il codice del ciclo. Quindi continua a scorrere come ti aspetteresti.

Ma al di là delle differenze di ramificazione causate dall'uso dei costrutti fore foreach, non vi era alcuna differenza nell'IL in base al luogo in cui int iera collocata la dichiarazione. Quindi siamo ancora ai due approcci che sono equivalenti.

Caso C: che dire delle diverse versioni del compilatore?

In un commento che era rimasto 1 , c'era un collegamento a una domanda SO riguardante un avviso sull'accesso variabile con foreach e l'uso della chiusura . La parte che ha davvero attirato la mia attenzione in quella domanda è che potrebbero esserci state differenze nel funzionamento del compilatore .NET 4.5 rispetto alle versioni precedenti del compilatore.

Ed è qui che il sito DotNetFiddler mi ha deluso: tutto ciò che avevano a disposizione era .NET 4.5 e una versione del compilatore Roslyn. Quindi ho richiamato un'istanza locale di Visual Studio e ho iniziato a testare il codice. Per essere sicuro che stavo confrontando le stesse cose, ho confrontato il codice creato localmente su .NET 4.5 con il codice DotNetFiddler.

L'unica differenza che ho notato era con il blocco init locale e la dichiarazione delle variabili. Il compilatore locale era un po 'più specifico nel nominare le variabili.

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

Ma con quella piccola differenza, era così lontano, così buono. Avevo un output IL equivalente tra il compilatore DotNetFiddler e ciò che la mia istanza VS locale stava producendo.

Quindi ho ricostruito il progetto destinato a .NET 4, .NET 3.5 e, per buona misura, alla modalità di rilascio di .NET 3.5.

E in tutti e tre questi casi aggiuntivi, l'IL generato era equivalente. La versione .NET mirata non ha avuto alcun effetto sull'IL che è stato generato in questi esempi.


Riassumendo questa avventura: penso che possiamo affermare con sicurezza che al compilatore non importa dove dichiari il tipo primitivo e che non c'è alcun effetto sulla memoria o sulle prestazioni con nessuno dei due metodi di dichiarazione. E questo vale indipendentemente dall'uso di a foro foreachloop.

Ho considerato di eseguire un altro caso che includeva una chiusura all'interno del foreachciclo. Ma avevi chiesto degli effetti di dove veniva dichiarata una variabile di tipo primitiva, quindi ho pensato che stavo scavando troppo oltre ciò di cui eri interessato. La domanda SO che ho menzionato in precedenza ha un'ottima risposta che fornisce una buona panoramica degli effetti di chiusura su ogni variabile di iterazione.

1 Grazie ad Andy per aver fornito il link originale alla domanda SO che affronta le chiusure all'interno dei foreachloop.

2 Vale la pena notare che la specifica ECMA-335 risolve questo problema con la sezione I.12.3.2.2 "Variabili e argomenti locali". Ho dovuto vedere l'IL risultante e quindi leggere la sezione per essere chiaro riguardo a ciò che stava succedendo. Grazie a ratchet maniaco per averlo sottolineato in chat.


1
For e foreach non si comportano allo stesso modo, e la domanda include un codice diverso che diventa importante quando c'è una chiusura nel loop. stackoverflow.com/questions/14907987/…
Andy,

1
@Andy - grazie per il link! Sono andato avanti e ho controllato l'output generato usando un foreachloop e ho anche verificato la versione .NET di destinazione.

0

A seconda del compilatore che usi (non so nemmeno se C # ne ha più di uno), il tuo codice verrà ottimizzato prima di essere trasformato in un programma. Un buon compilatore vedrà che stai inizializzando nuovamente la stessa variabile ogni volta con un valore diverso e gestendo lo spazio di memoria per esso in modo efficiente.

Se ogni volta inizializzavi la stessa variabile su una costante, il compilatore la inizializzerebbe allo stesso modo prima del ciclo e lo farebbe riferimento.

Tutto dipende da quanto è scritto bene il tuo compilatore, ma per quanto riguarda gli standard di codifica, le variabili dovrebbero sempre avere il minor ambito possibile . Quindi dichiarare all'interno del ciclo è ciò che mi è sempre stato insegnato.


3
Il fatto che il tuo ultimo paragrafo sia vero o no dipende da due cose: l'importanza di ridurre al minimo l'ambito della variabile all'interno del contesto unico del tuo programma e la conoscenza interna del compilatore per quanto riguarda l'ottimizzazione effettiva delle assegnazioni multiple.
Robert Harvey,

E poi c'è il runtime, che traduce ulteriormente il codice byte in linguaggio macchina, dove vengono eseguite anche molte di queste stesse ottimizzazioni (che vengono discusse qui come ottimizzazioni del compilatore).
Erik Eidt,

-2

in un primo momento stai solo dichiarando e inizializzando il ciclo interno, quindi ogni volta che il ciclo si riavvia verrà reinizializzato "i" all'interno del ciclo. In secondo luogo stai dichiarando solo al di fuori del ciclo.


1
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nella risposta migliore che è stata pubblicata più di 2 anni fa
moscerino

2
Grazie per aver dato una risposta, ma non fornisce a nessun nuovo aspetto la risposta accettata, la più votata non copre già (in dettaglio).
CharonX
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.