Déclarer une variable à l'intérieur ou à l'extérieur d'une boucle foreach: qu'est-ce qui est le plus rapide / le meilleur?

92

Lequel d'entre eux est le plus rapide / le meilleur?

Celui-là:

List<User> list = new List<User>();
User u;

foreach (string s in l)
{
    u = new User();
    u.Name = s;
    list.Add(u);
}

Ou celui-ci:

List<User> list = new List<User>();

foreach (string s in l)
{
    User u = new User();
    u.Name = s;
    list.Add(u);
}

Mes compétences en développement pour les débutants me disent que le premier est meilleur, mais un de mes amis me dit que je me trompe, mais ne peut pas me donner une bonne raison pour laquelle le second est meilleur.

Y a-t-il une différence de performance?

Marcus
la source

Réponses:

112

En termes de performances, les deux exemples sont compilés dans le même IL, il n'y a donc aucune différence.

La seconde est meilleure, car elle exprime plus clairement votre intention si elle un'est utilisée qu'à l'intérieur de la boucle.

dtb
la source
10
Notez qu'il ya est une différence si la variable est capturée par une expression lambda ou son délégué anonyme; voir Outer Variable Trap .
dtb
Pouvez-vous expliquer pourquoi les deux sont compilés dans le même IL? Je suis presque certain que C # ne hisse pas les déclarations de variables au sommet de la fonction comme le fait javascript.
styfle
4
@styfle voici la réponse à votre question.
David Sherret
Les liens suivants de Stack Overflow fournissent des réponses plus détaillées: 1) Jon Hanna et 2) StriplingWarrior
user3613932
14

Dans tous les cas, le meilleur moyen serait d'utiliser un constructeur qui prend un nom ... ou, sinon, d'exploiter la notation entre accolades:

foreach (string s in l)
{
    list.Add(new User(s));
}

ou

foreach (string s in l)
{
    list.Add(new User() { Name = s });
}

ou encore mieux, LINQ:

var list = l.Select( s => new User { Name = s});

Maintenant, alors que votre premier exemple pourrait, dans certains cas, être imperceptiblement plus rapide, le second est meilleur car il est plus lisible, et le compilateur peut rejeter la variable (et l'omettre complètement) car elle n'est pas utilisée en dehors de la foreachportée du.

Tordek
la source
6
Commentaire nécrophile du jour: "ou encore mieux, LINQ". Bien sûr, c'est une ligne de code, et cela nous fait nous sentir bien en tant que développeurs. Mais la version à quatre lignes est BEAUCOUP plus compréhensible, et donc maintenable.
Oskar Austegard
5
À peine. Avec la version LINQ, je sais que ce que je fais est immuable et que cela fonctionne sur chaque élément.
Tordek
6

Une déclaration ne provoque l'exécution d'aucun code, ce n'est donc pas un problème de performances.

Le deuxième est ce que vous voulez dire, et vous êtes moins susceptible de faire une erreur stupide si vous le faites de la deuxième façon, alors utilisez-le. Essayez toujours de déclarer des variables dans la plus petite portée nécessaire.

Et en plus, la meilleure façon est d'utiliser Linq:

List<User> users = l.Select(name => new User{ Name = name }).ToList();
Mark Byers
la source
2
J'adore la ligne "Essayez toujours de déclarer des variables dans la plus petite portée nécessaire." Je pense qu'une seule ligne peut très bien répondre à la question.
Manjoor
5

Chaque fois que vous avez une question sur les performances, la seule chose à faire est de mesurer - exécutez une boucle autour de votre test et chronométrez-le.

Pour répondre à votre question - sans mesurer :-) ou regarder l'ilasme généré - toute différence ne serait pas perceptible dans un nombre significatif d'itérations et l'opération la plus coûteuse dans votre code, il y aura probablement l'allocation de l'utilisateur par quelques commandes de magnitude, alors concentrez-vous sur la clarté du code (comme vous devriez en général) et allez avec 2.

Oh, il est tard et je suppose que j'essaie juste de dire ne vous inquiétez pas pour ce genre de choses ou ne vous laissez pas prendre par des détails comme celui-ci.

K

Kevin Shea
la source
merci pour le tuyau, je pense que je vais chronométrer d'autres choses sur lesquelles j'ai blessé ainsi hehe: D
Marcus
Si vous souhaitez aller plus loin dans l'analyse de ce qui affecte les performances, envisagez d'utiliser un profileur de code. Si rien d'autre, cela commencera à vous donner une idée du type de code et des opérations qui prendront le plus de temps. ProfileSharp et EqatecProfilers sont gratuits et suffisants pour vous aider à démarrer.
Kevin Shea
1

Le 2ème est meilleur. Vous voulez avoir un nouvel utilisateur à chaque itération.

Jarrett Widman
la source
1

Techniquement, le premier exemple économisera quelques nanosecondes car la trame de la pile n'aura pas à être déplacée pour allouer une nouvelle variable, mais c'est une si petite quantité de temps CPU que vous ne le remarquerez pas, c'est si le compilateur ne le fait pas. optimisez toute différence de toute façon.

Erik Funkenbusch
la source
Je suis assez sûr que le CLR n'alloue pas "une nouvelle variable" à chaque itération d'une boucle.
dtb
Eh bien, le compilateur peut très bien optimiser cela, mais l'espace de pile doit être alloué pour toutes les variables d'une boucle. Cela dépendra de l'implémentation et une implémentation peut simplement garder le cadre de la pile identique, tandis qu'une autre (par exemple Mono) pourrait libérer la pile puis la recréer sur chaque boucle.
Erik Funkenbusch
16
Toutes les variables locales d'une méthode (de niveau supérieur ou imbriquées dans une boucle) sont compilées en variables de niveau méthode dans IL. L'espace pour les variables est alloué avant l'exécution de la méthode, pas lorsqu'une branche avec la déclaration en C # est atteinte.
dtb
1
@dtb Avez-vous une source pour cette réclamation?
styfle
1

Dans ce scénario, la deuxième version est meilleure.

En général, si vous n'avez besoin d'accéder qu'à la valeur dans le corps de l'itération, choisissez la deuxième version. D'un autre côté, s'il y a un état final, la variable tiendra au-delà du corps de la boucle, puis déclare puis utilise la première version.

csj
la source
0

Il ne devrait y avoir aucune différence de performance perceptible.

Jacob Adams
la source
0

Je suis allé vérifier ce problème. Découvrant étonnamment lors de mes tests sales que la 2ème option est encore légèrement plus rapide tout le temps.

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    void Method1()
    {
      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }
    }

    void Method2()
    {

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }
    }
  }

  public class User { public string Name; }
}

J'ai vérifié le CIL mais ce n'est pas identique.

entrez la description de l'image ici

J'ai donc préparé quelque chose que je voulais être un bien meilleur test.

namespace Test
{
  class Loop
  { 

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    public void Method1()
    {
      sw.Restart();

      C c;
      C c1;
      C c2;
      C c3;
      C c4;

      int i = 1000;
      while (i-- > 0)
      {
        c = new C();
        c1 = new C();
        c2 = new C();
        c3 = new C();
        c4 = new C();        
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    public void Method2()
    {
      sw.Restart();

      int i = 1000;
      while (i-- > 0)
      {
        var c = new C();
        var c1 = new C();
        var c2 = new C();
        var c3 = new C();
        var c4 = new C();
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  class C { }
}

Dans ce cas également, la 2ème méthode était toujours gagnante, mais j'ai ensuite vérifié le CIL en ne trouvant aucune différence.

entrez la description de l'image ici

Je ne suis pas un gourou de la lecture CIL mais je ne vois aucun problème de déclinaison. Comme cela a déjà été souligné, la déclaration n'est pas une allocation, il n'y a donc pas de pénalité de performance.

Tester

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    void Method1()
    {
      sw.Restart();

      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    void Method2()
    {
      sw.Restart();

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  public class User { public string Name; }
Ucho
la source