220 votes

Performances des tableaux et des listes

Supposons que vous ayez besoin d'une liste/un tableau d'entiers que vous devez itérer fréquemment, et je veux dire extrêmement souvent. Les raisons peuvent varier, mais disons que c'est au cœur de la boucle la plus interne d'un traitement à haut volume.

En général, on opte pour l'utilisation des listes (List) en raison de leur flexibilité en termes de taille. De plus, la documentation msdn affirme que les listes utilisent un tableau en interne et devraient être aussi rapides (un rapide coup d'œil avec Reflector le confirme). Néanmoins, il y a une certaine surcharge.

Est-ce que quelqu'un a réellement mesuré cela ? Est-ce que itérer 6M fois dans une liste prendrait le même temps qu'un tableau ?

4 votes

En dehors des questions de performance, je préfère utiliser les tableaux plutôt que les listes pour leur taille fixe (dans les cas où il n'est pas nécessaire de changer le nombre d'éléments, bien sûr). Lorsque je lis du code existant, je trouve utile de savoir rapidement qu'un élément est forcé pour avoir une taille fixe, plutôt que de devoir inspecter le code plus bas dans la fonction.

3 votes

T[] vs. List<T> peut faire une grande différence en termes de performances. Je viens d'optimiser une application extrêmement intensive en boucles (imbriquées) pour passer des listes aux tableaux sur .NET 4.0. Je m'attendais à une amélioration de 5 à 10 %, mais j'ai obtenu une accélération de plus de 40 % ! Aucun autre changement que le passage direct de la liste au tableau. Toutes les énumérations ont été faites avec foreach déclarations. D'après la réponse de Marc Gravell, il semble que foreach con List<T> est particulièrement mauvais.

242voto

Marc Gravell Points 482669

Très facile à mesurer...

Dans un petit nombre de code de traitement en boucle serrée où je sais que la longueur est fixe J'utilise les tableaux pour ce petit plus de micro-optimisation ; les tableaux peuvent être marginalement plus rapide si vous utilisez l'indexeur / pour le formulaire - mais IIRC croit que cela dépend du type de données dans le tableau. Mais à moins que vous besoin de de micro-optimiser, de rester simple et d'utiliser List<T> etc.

Bien entendu, cela ne s'applique que si vous lisez toutes les données ; un dictionnaire serait plus rapide pour les recherches par clé.

Voici mes résultats en utilisant "int" (le deuxième chiffre est une somme de contrôle pour vérifier qu'ils ont tous fait le même travail) :

(édité pour corriger le bug)

List/for: 1971ms (589725196)
Array/for: 1864ms (589725196)
List/foreach: 3054ms (589725196)
Array/foreach: 1860ms (589725196)

basé sur le banc d'essai :

using System;
using System.Collections.Generic;
using System.Diagnostics;
static class Program
{
    static void Main()
    {
        List<int> list = new List<int>(6000000);
        Random rand = new Random(12345);
        for (int i = 0; i < 6000000; i++)
        {
            list.Add(rand.Next(5000));
        }
        int[] arr = list.ToArray();

        int chk = 0;
        Stopwatch watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = list.Count;
            for (int i = 0; i < len; i++)
            {
                chk += list[i];
            }
        }
        watch.Stop();
        Console.WriteLine("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            for (int i = 0; i < arr.Length; i++)
            {
                chk += arr[i];
            }
        }
        watch.Stop();
        Console.WriteLine("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in list)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in arr)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        Console.ReadLine();
    }
}

8 votes

Détail intéressant : Voici les temps RELEASE/DEBUG sur ma plate-forme (.net 3.5 sp1) : 0.92, 0.80, 0.96, 0.93 ; ce qui m'indique qu'il y a une certaine intelligence dans la VM pour optimiser la boucle Array/for environ 10% mieux que les autres cas.

2 votes

Oui, il y a une optimisation JIT pour array/for. En fait, j'avais l'impression qu'il inclus le cas Length (puisqu'il sait qu'il est fixé), d'où la raison pour laquelle je ne l'ai pas retiré en premier (contrairement à List où je l'ai fait). Merci pour la mise à jour.

0 votes

Oh, également la différence entre for y foreach est grandement diminuée si vous ne faites pas le double du travail dans ce cas ! (commencez par corriger l'initialisation en list.Add(rand.Next()); )

27voto

Frederik Gheysels Points 36354

Je pense que les performances seront assez similaires. La surcharge qui est impliquée dans l'utilisation d'une liste par rapport à un tableau est, à mon avis, lorsque vous ajoutez des éléments à la liste, et lorsque la liste doit augmenter la taille du tableau qu'elle utilise en interne, lorsque la capacité du tableau est atteinte.

Supposons que vous ayez une liste avec une capacité de 10, alors la liste augmentera sa capacité une fois que vous voudrez ajouter le 11ème élément. Vous pouvez réduire l'impact sur les performances en initialisant la capacité de la liste au nombre d'éléments qu'elle contiendra.

Mais, pour savoir si l'itération sur une liste est aussi rapide que l'itération sur un tableau, pourquoi ne pas l'évaluer ?

int numberOfElements = 6000000;

List<int> theList = new List<int> (numberOfElements);
int[] theArray = new int[numberOfElements];

for( int i = 0; i < numberOfElements; i++ )
{
    theList.Add (i);
    theArray[i] = i;
}

Stopwatch chrono = new Stopwatch ();

chrono.Start ();

int j;

 for( int i = 0; i < numberOfElements; i++ )
 {
     j = theList[i];
 }

 chrono.Stop ();
 Console.WriteLine (String.Format("iterating the List took {0} msec", chrono.ElapsedMilliseconds));

 chrono.Reset();

 chrono.Start();

 for( int i = 0; i < numberOfElements; i++ )
 {
     j = theArray[i];
 }

 chrono.Stop ();
 Console.WriteLine (String.Format("iterating the array took {0} msec", chrono.ElapsedMilliseconds));

 Console.ReadLine();

Sur mon système, l'itération sur le tableau a pris 33msec ; l'itération sur la liste a pris 66msec.

Pour être honnête, je ne m'attendais pas à ce que la variation soit si importante. Donc, j'ai mis mon itération dans une boucle : maintenant, j'exécute les deux itérations 1000 fois. Les résultats sont :

l'itération de la liste a pris 67146 msec L'itération du tableau prend 40821 msec

Aujourd'hui, l'écart n'est plus aussi important, mais il reste...

J'ai donc lancé .NET Reflector, et le getter de l'indexeur de la classe List ressemble à ceci :

public T get_Item(int index)
{
    if (index >= this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    return this._items[index];
}

Comme vous pouvez le constater, lorsque vous utilisez l'indexeur de la liste, cette dernière vérifie si vous ne sortez pas des limites du tableau interne. Cette vérification supplémentaire a un coût.

0 votes

Bonjour Frederik, Merci ! Comment expliquez-vous que votre liste prenne deux fois plus de temps que les tableaux. Ce n'est pas ce à quoi on pourrait s'attendre. Avez-vous essayé d'augmenter le nombre d'éléments ?

1 votes

Est-ce que le fait de retourner this._items[index] ; ne lèverait pas déjà une exception si l'index était hors de la plage ? Pourquoi .NET doit-il effectuer cette vérification supplémentaire alors que le résultat final est le même avec ou sans cette vérification ?

0 votes

John Mercier : la vérification porte sur la taille de la liste (nombre d'éléments actuellement contenus), qui est différente et probablement inférieure à la capacité du tableau _items. Le tableau est alloué avec une capacité excédentaire pour rendre l'ajout de futurs éléments plus rapide en ne nécessitant pas de réallocation pour chaque ajout.

22voto

ShuggyCoUk Points 24204

Si vous ne récupérez qu'une seule valeur de l'un ou l'autre (pas dans une boucle), les deux font le contrôle des limites (vous êtes dans du code géré, rappelez-vous), c'est juste la liste qui le fait deux fois. Voir les notes plus loin pour savoir pourquoi ce n'est pas un gros problème.

Si vous utilisez votre propre for(int i = 0 ; i < x.[Length/Count];i++), la différence essentielle est la suivante :

  • Array :
    • la vérification des limites est supprimée
  • Listes
    • la vérification des limites est effectuée

Si vous utilisez foreach, la différence essentielle est la suivante :

  • Array :
    • aucun objet n'est alloué pour gérer l'itération
    • la vérification des limites est supprimée
  • List via une variable connue pour être List.
    • la variable de gestion des itérations est allouée à la pile
    • la vérification des limites est effectuée
  • Liste via une variable connue pour être IList.
    • la variable de gestion des itérations est allouée au tas
    • la vérification des limites est effectuée De plus, les valeurs des listes ne peuvent pas être modifiées pendant le foreach alors que celles des tableaux peuvent l'être.

La vérification des limites n'est souvent pas un gros problème (surtout si vous êtes sur un processeur avec un pipeline profond et une prédiction de branchement - la norme pour la plupart de nos jours) mais seul votre propre profilage peut vous dire si c'est un problème. Si vous êtes dans des parties de votre code où vous évitez les allocations de tas (de bons exemples sont les bibliothèques ou les implémentations de hashcode), alors s'assurer que la variable est typée comme List et non IList évitera ce piège. Comme toujours, profitez-en si cela est important.

12voto

David Schmitt Points 29384

[ Voir aussi cette question ]

J'ai modifié la réponse de Marc pour utiliser des nombres aléatoires réels et faire le même travail dans tous les cas.

Résultats :

         for      foreach
Array : 1575ms     1575ms (+0%)
List  : 1630ms     2627ms (+61%)
         (+3%)     (+67%)

(Checksum: -1000038876)

Compilé comme Release sous VS 2008 SP1. Fonctionne sans débogage sur un Q6600@2.40GHz, .NET 3.5 SP1.

Code :

class Program
{
    static void Main(string[] args)
    {
        List<int> list = new List<int>(6000000);
        Random rand = new Random(1);
        for (int i = 0; i < 6000000; i++)
        {
            list.Add(rand.Next());
        }
        int[] arr = list.ToArray();

        int chk = 0;
        Stopwatch watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = list.Count;
            for (int i = 0; i < len; i++)
            {
                chk += list[i];
            }
        }
        watch.Stop();
        Console.WriteLine("List/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            int len = arr.Length;
            for (int i = 0; i < len; i++)
            {
                chk += arr[i];
            }
        }
        watch.Stop();
        Console.WriteLine("Array/for: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in list)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("List/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);

        chk = 0;
        watch = Stopwatch.StartNew();
        for (int rpt = 0; rpt < 100; rpt++)
        {
            foreach (int i in arr)
            {
                chk += i;
            }
        }
        watch.Stop();
        Console.WriteLine("Array/foreach: {0}ms ({1})", watch.ElapsedMilliseconds, chk);
        Console.WriteLine();

        Console.ReadLine();
    }
}

0 votes

C'est bizarre - je viens d'exécuter votre code exact, construit en ligne de commande (3.5SP1) avec /o+ /debug- et mes résultats sont : list/for : 1524 ; array/for : 1472 ; list/foreach:4128 ; array/foreach:1484.

1 votes

Vous dites qu'il a été compilé en tant que release - puis-je simplement vérifier que vous l'avez exécuté plutôt que de le déboguer ? Question stupide, je sais, mais je ne peux pas expliquer les résultats autrement...

2voto

Stephan Eggermont Points 11224

Les mesures sont intéressantes, mais vous obtiendrez des résultats très différents selon ce que vous faites exactement dans votre boucle intérieure. Mesurez votre propre situation. Si vous utilisez le multithreading, cela représente à lui seul une activité non triviale.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X