Quelle est la boucle la plus efficace en c#

il existe un certain nombre de façons différentes d'accomplir la même boucle simple à travers les éléments d'un objet en c#.

cela m'a fait me demander s'il y a une raison quelconque que ce soit la performance ou la facilité d'utilisation, comme d'utiliser sur l'autre. Ou est-ce dû à une préférence personnelle?

Prendre un objet simple

var myList = List<MyObject>; 

supposons que l'objet est rempli et que nous voulons itérer 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, c'est si chacun de ces éléments était compilé de la même façon? y a-t-il un avantage évident sur le plan de la performance à utiliser l'un par rapport aux autres?

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

Ai-je manqué?

24
demandé sur TheAlbear 2013-03-06 16:22:21

1 réponses

La réponse de la majorité du temps est il 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) ne va pas avoir d'impact sur le code.

bien sûr, si vous identifiez ce goulot d'étranglement dans votre situation, par tous les moyens, adressez-vous-en, mais vous devez d'abord identifier le goulot d'étranglement.

cela dit, il y a un certain nombre de choses à prendre en considération à 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 d'
  • tous les tests ont été effectués dans le cadre de séances d'essais unitaires distinctes, mais pas dans la même (afin de ne pas interférer avec le ramassage des ordures, etc.).)

Voici quelques assistants qui sont nécessaires pour chaque test:

MyObject catégorie:

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

une méthode pour créer Un List<T> d'une longueur de MyClass exemples:

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 à exécuter pour chaque élément de la liste (nécessaire car la méthode 2 utilise un délégué, et un appel doit être fait à 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 sur un null Stream (en gros une des données de l'évier):

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

et fixons le nombre d'items à un million (1.000.000, qui devraient être suffisamment élevés pour imposer que généralement, ceux-ci ont tous à peu près le même impact de performance):

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

prenons l'méthodes:

Méthode 1:foreach

Le code suivant:

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

Compile vers le bas dans le code suivant:

using (var enumerable = myList.GetEnumerable())
while (enumerable.MoveNext())
{
    var item = enumerable.Current;

    // Do stuff.
}

il y a tout un peu y passe. Vous avez les appels de méthode (et il peut ou ne peut pas être contre le IEnumerator<T> ou IEnumerator interfaces, comme le compilateur respecte canard-typage dans ce cas) et votre // Do stuff est hissé dans cette structure tandis que.

voici le test pour mesurer la performance:

[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:

boucle Foreach tiques: 3210872841

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

le code pour le .ForEach méthode List<T> ressemble à quelque chose comme 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 ceci est fonctionnellement équivalent à la méthode 4, avec une exception, le code qui est hissé dans le for boucle est passée en tant que délégué. Cela nécessite une déréférence pour accéder au code qui doit être exécuté. Alors que la performance des délégués s'est améliorée par rapport à .NET 3.0 on, cette overhead il n'.

cependant, c'est négligeable. Le test de mesure de la performance:

[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:

pour chaque méthode tiques: 3135132204

C'est en fait, ~7,5 secondes plus viteforeach boucle. Pas tout à fait surprenant, étant donné qu'il utilise directement accès au tableau au lieu d'utiliser IEnumerable<T>.

rappelez-vous cependant, cela se traduit à 0.0000075740637 secondes par article étant enregistré. C'est la peine pour les petites listes d'éléments.

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

comme le montre la méthode 1, c'est exactement ce que fait le compilateur (avec l'ajout du using déclaration, ce qui est une bonne pratique). Vous ne gagnez rien ici en déballant vous-même le code que le compilateur générerait autrement.

Pour les coups de pied, nous allons le faire de toute façon:

[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:

boucle des recenseurs tics: 3241289895

méthode 4:for

dans ce cas particulier, vous allez gagner de la vitesse, car l'indexeur de liste va directement vers le tableau sous-jacent pour effectuer la recherche (c'est un détail d'implémentation, BTW, il n'y a rien à dire que cela ne peut pas être une structure arborescente soutenant le 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 de l'indexeur boucle tiques: 3039649305

cependant au lieu de faire une différence est 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 item dans une boucle de dix items, le compilateur peut décompresser ceci en cinq itérations de deux items dans une boucle de dix items.

cependant, je ne suis pas certain ici que cela se passe réellement (je dois regarder L'IL et le 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:

tableau loop ticks: 3102911316

Il est à noter que out-of-the-box, Resharper propose une suggestion avec un remaniement pour changer ce qui précède for instructions foreach consolidés. Cela ne veut pas dire que c'est juste, mais la base est de réduire le montant de la dette technique dans code.


TL;DR

vous ne devriez vraiment pas être préoccupé par la performance de ces choses, à moins que des tests dans votre situation montre que vous avez un réel goulot d'étranglement (et vous devrez avoir un nombre massif d'articles pour avoir un impact).

généralement, vous devez choisir ce qui est le plus maintenable, auquel cas, la méthode 1 (foreach) est la voie à suivre.

43
répondu casperOne 2013-04-26 13:28:24