web-dev-qa-db-fra.com

Quelle est la boucle la plus efficace en c #

Il existe différentes manières de réaliser la même boucle simple à travers les éléments d'un objet en c #.

Cela m'a amené à me demander s'il y a une raison, que ce soit la performance ou la facilité d'utilisation, quant à une utilisation par rapport à l'autre. Ou est-ce simplement par préférence personnelle.

Prenez un objet simple

var myList = List<MyObject>; 

Supposons que l'objet est rempli et nous voulons 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   
}

Je me demandais si chacun de ces éléments était compilé pour la même chose? Existe-t-il un avantage clair en termes de performances pour l’utilisation de l’une sur les autres?

ou s'agit-il simplement d'une préférence personnelle lors du codage?

En ai-je manqué?

28
TheAlbear

La réponse la plupart du temps est peu importe. Le nombre d'éléments dans la boucle (même ce que l'on pourrait considérer comme un "grand" nombre de éléments, par exemple des milliers) ne va pas avoir un impact sur le code.

Bien sûr, si vous identifiez cela comme un goulot d'étranglement dans votre situation, résolvez-le, mais vous devez d'abord identifier le goulot d'étranglement.

Cela dit, il y a un certain nombre de choses à prendre en considération avec chaque approche, que je vais décrire ici.

Définissons d'abord quelques éléments:

  • 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é effectués dans des sessions de test unitaires distinctes, pas dans la même (afin de ne pas interférer avec les récupérations de place, 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 un List<T> de n'importe quelle longueur de MyClass instances:

public static List<MyObject> 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();
}

Une action à effectuer pour chaque élément de la liste (nécessaire car la méthode 2 utilise un délégué et un appel doit être effectué pour 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 nullStream (essentiellement un récepteur 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'éléments à un million (1 000 000, ce qui devrait être suffisamment élevé pour que, d'une manière générale, tous aient à peu près 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
}

Compile les informations suivantes:

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 de méthode (et cela peut ou non être contre les interfaces IEnumerator<T> Ou IEnumerator, car le compilateur respecte le typage canard dans ce cas) et votre // Do stuff Est hissé dans ce tout structure.

Voici le test pour mesurer les performances:

[TestMethod]
public void TestForEachKeyword()
{
    // Create the list.
    List<MyObject> 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:

Tiques de boucle Foreach: 3210872841

Méthode 2: méthode .ForEach Sur List<T>

Le code de la méthode .ForEach Sur List<T> Ressemble à ceci:

public void ForEach(Action<T> 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 passé en tant que délégué. Cela nécessite une déréférence pour accéder au code qui doit être exécuté. Bien que les performances des délégués se soient améliorées à partir de .NET 3.0, cette surcharge est là-bas.

Cependant, c'est négligeable. Le test pour mesurer les performances:

[TestMethod]
public void TestForEachMethod()
{
    // Create the list.
    List<MyObject> 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:

ForEach méthode ticks: 3135132204

C'est en fait ~ 7,5 secondes plus rapide que d'utiliser la boucle foreach. Pas complètement surprenant, étant donné qu'il utilise un accès direct au tableau au lieu d'utiliser IEnumerable<T> .

N'oubliez pas cependant que cela se traduit par 0,0000075740637 secondes par élément en cours d'enregistrement. C'est pas ça vaut le coup pour de petites listes d'articles.

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

Comme le montre 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éroulant vous-même le code que le compilateur générerait autrement.

Pour les coups de pied, faisons-le quand même:

[TestMethod]
public void TestEnumerator()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    // Get the enumerator.
    using (IEnumerator<MyObject> 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:

Tiques de la boucle d'énumérateur: 3241289895

Méthode 4: for

Dans ce cas particulier, vous allez gagner en vitesse, car l'indexeur de liste va directement au tableau sous-jacent pour effectuer la recherche (c'est un détail d'implémentation, BTW, il n'y a rien à dire qu'il ne peut pas s'agir d'une structure arborescente sauvegarde du List<T>).

[TestMethod]
public void TestListIndexer()
{
    // Create the list.
    List<MyObject> 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 la boucle d'indexation: 3039649305

Cependant l'endroit où cela peut faire la différence est les tableaux. Les tableaux peuvent être déroulés par le compilateur pour traiter plusieurs éléments à la fois.

Au lieu de faire dix itérations d'un élément dans une boucle de dix éléments, le compilateur peut dérouler cela en cinq itérations de deux éléments dans une boucle de dix éléments.

Cependant, je ne suis pas sûr ici que cela se produise réellement (je dois regarder l'IL et la sortie 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:

Tiques de boucle de tableau: 3102911316

Il convient de noter que, prêt à l'emploi, Resharper propose une suggestion avec une refactorisation pour remplacer les instructions for ci-dessus par des instructions 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 soucier de la performance de ces choses, à moins que les tests dans votre situation ne montrent que vous avez un véritable goulot d'étranglement (et que vous devrez avoir un nombre énorme d'éléments pour avoir un impact).

En règle générale, vous devez opter pour ce qui est le plus maintenable, auquel cas, la méthode 1 (foreach) est la voie à suivre.

54
casperOne

En ce qui concerne la dernière partie de la question, "en ai-je manqué?" oui et je pense que je m'en voudrais de ne pas mentionner ici même si la question est assez ancienne. Bien que ces quatre façons de le faire s'exécuteront relativement dans le même laps de temps, il s'agit d'un moyen non illustré ci-dessus qui s'exécute plus rapidement que tous, ce qui est assez significatif en fait à mesure que la taille de la liste qui est itérée augmente. Ce serait exactement la même manière que la dernière méthode, mais au lieu d'obtenir .Count dans la vérification de condition de la boucle, vous affectez cette valeur à la variable avant de configurer la boucle et utilisez-la à la place, vous laissant avec quelque chose comme ça

var countVar = list.Count;
for(int i = 0; i < countVar; i++)
{
 //loop logic
}

en procédant de cette façon, vous recherchez uniquement une valeur de variable à chaque itération, plutôt que de résoudre les propriétés Count ou Length, ce qui est considérablement moins efficace.

2
nickw