Quelle est la boucle la plus efficace en c #

Il existe différentes méthodes pour réaliser la même boucle simple à travers les éléments d’un object en c #.

Cela m’a fait me demander s’il y avait une raison, que ce soit la performance ou la facilité d’utilisation, à utiliser sur l’autre. Ou est-ce juste par préférence personnelle.

Prendre un object simple

var myList = List; 

Supposons que l’object est rempli et que nous souhaitons parcourir les éléments.

Méthode 1

 foreach(var item in myList) { //Do stuff } 

Méthode 2

 myList.Foreach(ml => { //Do stuff }); 

Méthode 3

 while (myList.MoveNext()) { //Do stuff } 

Méthode 4

 for (int i = 0; i < myList.Count; i++) { //Do stuff } 

Ce que je me demandais, est-ce que chacun de ces éléments est compilé à la même chose? Existe-t-il un avantage clair en termes de performances pour l’utilisation de l’une sur les autres?

ou est-ce simplement une préférence personnelle lors du codage?

En ai-je manqué?

La réponse la plupart du temps est que cela n’a pas d’importance. Le nombre d’éléments dans la boucle (même ce que l’on pourrait considérer comme un “grand” nombre d’éléments, disons des milliers) n’aura pas d’impact sur le code.

Bien sûr, si vous identifiez cela comme un goulot d’étranglement dans votre situation, vous devez absolument vous en occuper, mais vous devez d’abord identifier le goulot d’étranglement.

Cela dit, il y a un certain nombre de facteurs à prendre en compte dans chaque approche, que je vais décrire ici.

Définissons d’abord quelques choses:

  • Tous les tests ont été exécutés sur .NET 4.0 sur un processeur 32 bits.
  • TimeSpan.TicksPerSecond sur ma machine = 10 000 000
  • Tous les tests ont été réalisés dans des sessions de tests unitaires distinctes, et non dans le même (afin de ne pas interférer avec le nettoyage de la mémoire, etc.).

Voici quelques aides nécessaires pour chaque test:

La classe MyObject :

 public class MyObject { public int IntValue { get; set; } public double DoubleValue { get; set; } } 

Une méthode pour créer une List de n’importe quelle longueur d’instances de MyClass :

 public static List CreateList(int items) { // Validate parmaeters. if (items < 0) throw new ArgumentOutOfRangeException("items", items, "The items parameter must be a non-negative value."); // Return the items in a list. return Enumerable.Range(0, items). Select(i => new MyObject { IntValue = i, DoubleValue = i }). ToList(); } 

Action à effectuer pour chaque élément de la liste (nécessaire car la méthode 2 utilise un délégué et qu’un appel doit être effectué sur quelque chose pour mesurer l’impact):

 public static void MyObjectAction(MyObject obj, TextWriter writer) { // Validate parameters. Debug.Assert(obj != null); Debug.Assert(writer != null); // Write. writer.WriteLine("MyObject.IntValue: {0}, MyObject.DoubleValue: {1}", obj.IntValue, obj.DoubleValue); } 

Une méthode pour créer un TextWriter qui écrit dans un Stream nul (essentiellement un collecteur de données):

 public static TextWriter CreateNullTextWriter() { // Create a stream writer off a null stream. return new StreamWriter(Stream.Null); } 

Et fixons le nombre d’articles à un million (1 000 000, ce qui devrait être suffisamment élevé pour faire respecter le fait qu’ils ont généralement le même impact sur les performances):

 // The number of items to test. public const int ItemsToTest = 1000000; 

Entrons dans les méthodes:

Méthode 1: foreach

Le code suivant:

 foreach(var item in myList) { //Do stuff } 

Comstack dans les éléments suivants:

 using (var enumerable = myList.GetEnumerable()) while (enumerable.MoveNext()) { var item = enumerable.Current; // Do stuff. } 

Il se passe pas mal de choses là-bas. Vous avez les appels à la méthode (et cela peut ou non être contre les interfaces IEnumerator ou IEnumerator , car le compilateur respecte le typage de canard dans ce cas) et votre // Do stuff est hissé dans cette structure while.

Voici le test pour mesurer la performance:

 [TestMethod] public void TestForEachKeyword() { // Create the list. List list = CreateList(ItemsToTest); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) { // Create the stopwatch. Stopwatch s = Stopwatch.StartNew(); // Cycle through the items. foreach (var item in list) { // Write the values. MyObjectAction(item, writer); } // Write out the number of ticks. Debug.WriteLine("Foreach loop ticks: {0}", s.ElapsedTicks); } } 

Le résultat:

Ticks de boucle Foreach: 3210872841

Méthode 2: Méthode .ForEach de la List

Le code de la méthode .ForEach de la List ressemble à ceci:

 public void ForEach(Action action) { // Error handling omitted // Cycle through the items, perform action. for (int index = 0; index < Count; ++index) { // Perform action. action(this[index]); } } 

Notez que cela est fonctionnellement équivalent à la méthode 4, à une exception près, le code qui est hissé dans la boucle for est transmis en tant que délégué. Cela nécessite une déréférence pour obtenir le code qui doit être exécuté. Bien que les performances des delegates se soient améliorées à partir de .NET 3.0, cette surcharge existe.

Cependant, c'est négligeable. Le test pour mesurer la performance:

 [TestMethod] public void TestForEachMethod() { // Create the list. List list = CreateList(ItemsToTest); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) { // Create the stopwatch. Stopwatch s = Stopwatch.StartNew(); // Cycle through the items. list.ForEach(i => MyObjectAction(i, writer)); // Write out the number of ticks. Debug.WriteLine("ForEach method ticks: {0}", s.ElapsedTicks); } } 

Le résultat:

Pour chaque méthode: 3135132204

C'est en réalité environ 7,5 secondes plus rapide que d'utiliser la boucle foreach . Pas tout à fait surprenant, car il utilise un access direct à un tableau au lieu d’utiliser IEnumerable .

Rappelez-vous cependant que cela se traduit par 0,0000075740637 secondes par élément enregistré. Cela ne vaut pas la peine pour de petites listes d'articles.

Méthode 3: while (myList.MoveNext())

Comme indiqué dans la méthode 1, c'est exactement ce que fait le compilateur (avec l'ajout de l'instruction using , ce qui est une bonne pratique). Vous ne gagnez rien ici en décompressant vous-même le code que le compilateur générerait autrement.

Pour les kicks, faisons-le quand même:

 [TestMethod] public void TestEnumerator() { // Create the list. List list = CreateList(ItemsToTest); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) // Get the enumerator. using (IEnumerator enumerator = list.GetEnumerator()) { // Create the stopwatch. Stopwatch s = Stopwatch.StartNew(); // Cycle through the items. while (enumerator.MoveNext()) { // Write. MyObjectAction(enumerator.Current, writer); } // Write out the number of ticks. Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks); } } 

Le résultat:

Ticks de boucle de l'énumérateur: 3241289895

Méthode 4: for

Dans ce cas particulier, vous allez gagner du temps, car l'indexeur de liste se rend directement au tableau sous-jacent pour effectuer la recherche (détail de l'implémentation, BTW, rien ne dit qu'il ne peut s'agir d'une structure arborescente). sauvegarder la List vers le haut).

 [TestMethod] public void TestListIndexer() { // Create the list. List list = CreateList(ItemsToTest); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) { // Create the stopwatch. Stopwatch s = Stopwatch.StartNew(); // Cycle by index. for (int i = 0; i < list.Count; ++i) { // Get the item. MyObject item = list[i]; // Perform the action. MyObjectAction(item, writer); } // Write out the number of ticks. Debug.WriteLine("List indexer loop ticks: {0}", s.ElapsedTicks); } } 

Le résultat:

Liste des ticks de boucle d'indexeur: 3039649305

Cependant, l'endroit où cela peut faire la différence est les tableaux. Le compilateur peut dérouler des tableaux pour traiter plusieurs éléments à la fois.

Au lieu d'effectuer dix itérations d'un élément dans une boucle de dix éléments, le compilateur peut décomposer cette opération en cinq itérations de deux éléments dans une boucle de dix éléments.

Cependant, je ne suis pas sûr que cela se produise réellement (je dois regarder l'IL et le résultat de l'IL compilé).

Voici le test:

 [TestMethod] public void TestArray() { // Create the list. MyObject[] array = CreateList(ItemsToTest).ToArray(); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) { // Create the stopwatch. Stopwatch s = Stopwatch.StartNew(); // Cycle by index. for (int i = 0; i < array.Length; ++i) { // Get the item. MyObject item = array[i]; // Perform the action. MyObjectAction(item, writer); } // Write out the number of ticks. Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks); } } 

Le résultat:

Ticks de boucle de tableau: 3102911316

Il convient de noter que dès sa sortie de la boîte, Resharper propose une refactorisation pour modifier ce qui précède for déclarations en déclarations foreach . Cela ne veut pas dire que c'est juste, mais la base est de réduire le montant de la dette technique dans le code.


TL; DR

Vous ne devriez vraiment pas vous préoccuper de la performance de ces choses, à moins que des tests effectués dans votre situation montrent que vous avez un réel goulot d'étranglement (et que vous aurez besoin d'un grand nombre d'articles pour avoir un impact).

En règle générale, vous devriez rechercher ce qui est le plus facile à gérer. Dans ce cas, la méthode 1 ( foreach ) est la voie à suivre.