Boucle Foreach et initialisation variable

11

Y a-t-il une différence entre ces deux versions de code?

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

Ou le compilateur s'en fiche-t-il? Quand je parle de différence, je veux dire en termes de performances et d'utilisation de la mémoire. .. Ou fondamentalement n'importe quelle différence ou les deux finissent-ils par être le même code après la compilation?

Alternatex
la source
6
Avez-vous essayé de compiler les deux et de regarder la sortie du bytecode?
4
@MichaelT Je n'ai pas l'impression d'être qualifié pour comparer la sortie de bytecode .. Si je trouve une différence, je ne suis pas sûr de pouvoir comprendre ce que cela signifie exactement.
Alternatex
4
Si c'est pareil, vous n'avez pas besoin d'être qualifié.
1
@MichaelT Bien que vous deviez être suffisamment qualifié pour savoir si le compilateur aurait pu l'optimiser, et si oui dans quelles conditions il est capable de faire cette optimisation.
Ben Aaronson
@BenAaronson et cela nécessite probablement un exemple non trivial pour chatouiller cette fonctionnalité.

Réponses:

22

TL; DR - ce sont des exemples équivalents au niveau de la couche IL.


DotNetFiddle rend cette jolie réponse car elle vous permet de voir l'IL résultant.

J'ai utilisé une variation légèrement différente de votre construction de boucle afin de rendre mes tests plus rapides. J'ai utilisé:

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

Dans les deux cas, la sortie IL compilée est restée la même.

.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

Donc pour répondre à votre question: le compilateur optimise la déclaration de la variable, et rend les deux variantes équivalentes.

À ma connaissance, le compilateur .NET IL déplace toutes les déclarations de variables au début de la fonction, mais je n'ai pas pu trouver une bonne source qui indique clairement que 2 . Dans cet exemple particulier, vous voyez qu'il les a déplacés vers le haut avec cette instruction:

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

Où nous devenons un peu trop obsessionnels pour faire des comparaisons ...

Cas A, toutes les variables sont-elles déplacées vers le haut?

Pour creuser un peu plus, j'ai testé la fonction suivante:

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 différence ici est que nous déclarons un int iou un string jbasé sur la comparaison. Encore une fois, le compilateur déplace toutes les variables locales en haut de la fonction 2 avec:

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

J'ai trouvé intéressant de noter que même s'il int ine sera pas déclaré dans cet exemple, le code pour le prendre en charge est toujours généré.

Cas B: Et au foreachlieu de for?

Il a été souligné que le foreachcomportement est différent de forcelui et que je ne vérifiais pas la même chose qui avait été posée. J'ai donc mis ces deux sections de code pour comparer l'IL résultant.

int déclaration en dehors de la boucle:

    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 déclaration à l'intérieur de la boucle:

    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 résultant avec la foreachboucle était en effet différent de l'IL généré en utilisant la forboucle. Plus précisément, le bloc init et la section de boucle ont changé.

.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' foreachapproche a généré plus de variables locales et a nécessité des branchements supplémentaires. Essentiellement, la première fois, il saute à la fin de la boucle pour obtenir la première itération de l'énumération, puis revient presque au sommet de la boucle pour exécuter le code de la boucle. Il continue ensuite à boucler comme prévu.

Mais au-delà des différences de branchement causées par l'utilisation des constructions foret foreach, il n'y avait aucune différence dans l'IL en fonction de l'emplacement de la int idéclaration. Nous sommes donc toujours aux deux approches étant équivalentes.

Cas C: Qu'en est-il des différentes versions du compilateur?

Dans un commentaire qui a été laissé 1 , il y avait un lien vers une question SO concernant un avertissement concernant l'accès variable avec foreach et l'utilisation de la fermeture . La partie qui a vraiment attiré mon attention dans cette question était qu'il y avait peut-être des différences dans le fonctionnement du compilateur .NET 4.5 par rapport aux versions antérieures du compilateur.

Et c'est là que le site DotNetFiddler m'a laissé tomber - tout ce qu'ils avaient à disposition était .NET 4.5 et une version du compilateur Roslyn. J'ai donc mis en place une instance locale de Visual Studio et commencé à tester le code. Pour m'assurer que je comparais les mêmes choses, j'ai comparé le code construit localement à .NET 4.5 au code DotNetFiddler.

La seule différence que j'ai notée était avec le bloc init local et la déclaration de variable. Le compilateur local était un peu plus spécifique pour nommer les variables.

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

Mais avec cette petite différence, c'était si loin, tellement bon. J'avais une sortie IL équivalente entre le compilateur DotNetFiddler et ce que mon instance VS locale produisait.

J'ai donc reconstruit le projet en ciblant .NET 4, .NET 3.5, et pour faire bonne mesure .NET 3.5 Release mode.

Et dans ces trois cas supplémentaires, l'IL généré était équivalent. La version .NET ciblée n'a eu aucun effet sur l'IL généré dans ces échantillons.


Pour résumer cette aventure: je pense que nous pouvons dire avec confiance que le compilateur ne se soucie pas de l'endroit où vous déclarez le type primitif et qu'il n'y a aucun effet sur la mémoire ou les performances avec l'une ou l'autre méthode de déclaration. Et cela reste vrai indépendamment de l'utilisation d'une boucle forou foreach.

J'ai envisagé de lancer un autre cas qui incorporait une fermeture à l'intérieur de la foreachboucle. Mais vous aviez posé des questions sur les effets de la déclaration d'une variable de type primitif, alors j'ai pensé que j'allais trop loin au-delà de ce que vous vouliez savoir. La question SO que j'ai mentionnée plus tôt a une excellente réponse qui fournit un bon aperçu des effets de fermeture sur les variables d'itération foreach.

1 Merci à Andy d'avoir fourni le lien d'origine vers la question SO traitant des fermetures dans les foreachboucles.

2 Il convient de noter que la spécification ECMA-335 traite de cela avec la section I.12.3.2.2 «Variables et arguments locaux». J'ai dû voir l'IL obtenu et lire la section pour qu'il soit clair sur ce qui se passait. Merci à Ratchet Freak d'avoir signalé cela dans le chat.

Communauté
la source
1
For et foreach ne se comportent pas de la même manière, et la question inclut un code différent qui devient important lorsqu'il y a une fermeture dans la boucle. stackoverflow.com/questions/14907987/…
Andy
1
@Andy - merci pour le lien! Je suis allé de l'avant et j'ai vérifié la sortie générée à l'aide d'une foreachboucle et j'ai également vérifié la version .NET ciblée.
0

Selon le compilateur que vous utilisez (je ne sais même pas si C # en a plus d'un), votre code sera optimisé avant d'être transformé en programme. Un bon compilateur verra que vous réinitialisez la même variable à chaque fois avec une valeur différente et gérez efficacement l'espace mémoire pour cela.

Si vous initialisiez la même variable à une constante à chaque fois, le compilateur l'initialisait également avant la boucle et la référençait.

Tout dépend de la qualité de l'écriture de votre compilateur, mais en ce qui concerne les normes de codage, les variables doivent toujours avoir la portée la plus faible possible . Donc, déclarer à l'intérieur de la boucle est ce que j'ai toujours appris.

leylandski
la source
3
Que votre dernier paragraphe soit vrai ou non dépend de deux choses: l'importance de minimiser la portée de la variable dans le contexte unique de votre propre programme et la connaissance interne du compilateur pour savoir s'il optimise ou non les multiples affectations.
Robert Harvey
Et puis il y a le runtime, qui traduit davantage le code d'octet en langage machine, où bon nombre de ces mêmes optimisations (décrites ici comme optimisations du compilateur) sont également effectuées.
Erik Eidt
-2

au début, vous déclarez et initialisez simplement la boucle intérieure de sorte que chaque boucle de temps sera réinitialisée à l'intérieur de la boucle "i". En second lieu, vous déclarez uniquement en dehors de la boucle.

user304046
la source
1
cela ne semble pas offrir quoi que ce soit de substantiel par rapport aux points avancés et expliqués dans la réponse la plus haute qui a été publiée il y a plus de 2 ans
moucher
2
Merci d'avoir donné une réponse, mais cela ne donne pas de nouveaux aspects à la réponse acceptée et la mieux notée ne couvre pas déjà (en détail).
CharonX