11 Fév 2015

[Perf] C# : For ou Foreach, quel est le plus fort ?

Il s’agit d’une base algorithmique, mais on se pose rarement la question de savoir quelle boucle est la plus efficace. Y-a-t-il des différences ? Pouvons-nous améliorer les performances de nos programmes, de nos sites ?

Les bases 

Revenons aux bases. Nous utilisons des boucles « for » et « foreach » pour parcourir des ensembles d’éléments.

Le « for » est composé de trois éléments :

  • L’initialisation: permet d’initialiser la boucle (en déclarant une variable, en appelant une méthode…), avant le commencement de la boucle.
  • La condition : est vérifié avant chaque début de boucle. Cette condition permet de continuer / arrêter la boucle.
  • L’itérateur : permet de définir ce qui se passe en fin de boucle (assignation, appel de méthode, incrémentation, décrémentation, …).

« foreach » signifie « pour chaque ». On définit une variable d’itération suivi du mot clé « in » et de l’objet sur lequel on boucle. (l’objet peut être une collection ou un tableau). Exemple :

 

Performances / benchmark 

Dans quel cas utiliser un for et dans quel cas utiliser un foreach ? Pour le savoir, nous avons mis les deux instructions au banc d’essai.

Premier test :

  • Nous avons créé un tableau de 1000 entiers.
  • Nous avons appelés 1 000 000 de fois chacune des méthodes de parcours avec le tableau d’entiers.
  • Nous avons testé de parcourir le tableau avec un for incrémental, un for décrémental et un foreach

 

Les résultats (en débug) sont les suivant :

For incrémental : 3731 ms
For décrémental : 3375 ms
Foreach : 4761 ms

Remarques:

Allons voir à quoi ressemble le code une fois compilé :

 

On Remarque que le foreach est automatiquement transformé en for incremental et qu’il affecte la valeur courante dans une variable. On comprend rapidement pourquoi le code du foreach est plus lent du fait de l’affectation de cet variable temporaire.

Alors Pourquoi le For décrémental est-il plus rapide que le for incrémental : La réponse est dans la partie conditionnelle de la boucle. En effet nos processeurs ont la particularité d’être optimisés pour tester la valeur « 0 ». Ainsi l’écart de performance n’a rien à voir avec le sens de parcours mais bien avec le test qui est effectué à chaque tour de boucle.

Par contre en release, l’écart est moins significatif entre les 3 méthodes.

Allons au-delà de notre réflexion. Si le foreach est remplacé par un for incrémental, et que la faiblesse de ce « foreach » provient de l’assignation d’une variable locale ; Alors on peut se poser la question de l’utilité de cette variable.
Faisons alors le même test, mais cette fois ci en accédant deux fois à l’élément courant.

Résultats en debug :

For: 4035 ms
Foreach: 3554 ms

Dans ce cas, on se rend compte que le résultat est meilleur avec un foreach qu’avec un for incrémental. Ce qui signifie qu’il est préférable d’utiliser un foreach dès lors que l’on accède plus d’une fois à la valeur courante. Ce qui coûte chère est donc d’accéder à la donnée dans le tableau.

Par contre en release, l’écart n’est pas significatif.

 

Second Test :

Allons plus loin, l’écart n’étant pas significatif en release, cherchons les cas où ça l’est.
Tentons de changer de structure de données :

1er test : Au lieu d’utiliser un tableau nous utiliserons une liste

2nd test : Au lieu d’utiliser un entier (int), utilisons une classe (composée d’un int, d’un string, et d’un guid).

Ces tests ont tous été réalisés en release :

For For décremental Foreach
int[] 7019 6959 6953
List<int> 10473 10411 48147

Moyenne de 10 tests d’1 million de boucles itérées sur 10 000 éléments. Résultats en ms.

 

For For decremental Foreach
Class[] 1694 1694 1691
List<Class> 2452 2450 6921

Moyenne de 10 tests d’1 million de boucles de 1000 éléments. Résultats en ms.

Remarques :

  • L’utilisation d’un tableau est préférable à une List.
  • Dans le cas d’un tableau, le foreach est transformé en for par le compilateur ce qui explique le peu d’écart.
  • L’utilisation d’un foreach sur une List est bien plus coûteuse qu’un simple for (plus de 4 fois plus lent). Cette lenteur s’explique par le fait que le foreach sur une liste est remplacé par un énumerator, et que celui-ci appelle des méthodes getCurrent et moveNext.

 

Conclusion

  • Si vous n’avez pas de traitement spécifique impliquant une modification de la taille de la collection. Préférez utilisez un tableau plutôt qu’une Liste car le parcours est plus rapide.
  • Si vous parcourez une liste, préférez toujours un For au Foreach. (donc n’utilisez le foreach que pour un tableau)
  • L’accès à la donnée de la collection est couteux, utilisez une variable si vous voulez y accéder plusieurs fois (c’est ce que fait le foreach).

Ces gains semblent faibles mais si vous multipliez par le nombre d’utilisation par jours par utilisateur alors ça peut représenter beaucoup. Cela va également dans le sens d’une meilleure efficience énergétique.

 

Share