web-dev-qa-db-fra.com

Différences de performances ... si dramatiques?

Je viens de lire quelques articles sur List<T> vs LinkedList<T> , alors j’ai décidé de comparer moi-même certaines structures. J'ai comparé Stack<T>, Queue<T>, List<T> et LinkedList<T> en ajoutant des données et en supprimant des données vers/depuis le début/la fin. Voici le résultat de référence:

               Pushing to Stack...  Time used:      7067 ticks
              Poping from Stack...  Time used:      2508 ticks

               Enqueue to Queue...  Time used:      7509 ticks
             Dequeue from Queue...  Time used:      2973 ticks

    Insert to List at the front...  Time used:   5211897 ticks
RemoveAt from List at the front...  Time used:   5198380 ticks

         Add to List at the end...  Time used:      5691 ticks
  RemoveAt from List at the end...  Time used:      3484 ticks

         AddFirst to LinkedList...  Time used:     14057 ticks
    RemoveFirst from LinkedList...  Time used:      5132 ticks

          AddLast to LinkedList...  Time used:      9294 ticks
     RemoveLast from LinkedList...  Time used:      4414 ticks

Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Benchmarking
{
    static class Collections
    {
        public static void run()
        {
            Random Rand = new Random();
            Stopwatch sw = new Stopwatch();
            Stack<int> stack = new Stack<int>();
            Queue<int> queue = new Queue<int>();
            List<int> list1 = new List<int>();
            List<int> list2 = new List<int>();
            LinkedList<int> linkedlist1 = new LinkedList<int>();
            LinkedList<int> linkedlist2 = new LinkedList<int>();
            int dummy;


            sw.Reset();
            Console.Write("{0,40}", "Pushing to Stack...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                stack.Push(Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "Poping from Stack...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = stack.Pop();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Enqueue to Queue...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                queue.Enqueue(Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "Dequeue from Queue...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = queue.Dequeue();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Insert to List at the front...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                list1.Insert(0, Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveAt from List at the front...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = list1[0];
                list1.RemoveAt(0);
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "Add to List at the end...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                list2.Add(Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveAt from List at the end...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = list2[list2.Count - 1];
                list2.RemoveAt(list2.Count - 1);
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "AddFirst to LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                linkedlist1.AddFirst(Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveFirst from LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = linkedlist1.First.Value;
                linkedlist1.RemoveFirst();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);


            sw.Reset();
            Console.Write("{0,40}", "AddLast to LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                linkedlist2.AddLast(Rand.Next());
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks", sw.ElapsedTicks);
            sw.Reset();
            Console.Write("{0,40}", "RemoveLast from LinkedList...");
            sw.Start();
            for (int i = 0; i < 100000; i++)
            {
                dummy = linkedlist2.Last.Value;
                linkedlist2.RemoveLast();
                dummy++;
            }
            sw.Stop();
            Console.WriteLine("  Time used: {0,9} ticks\n", sw.ElapsedTicks);
        }
    }
}

Les différences sont so dramatique!

Comme vous pouvez le constater, les performances de Stack<T> et Queue<T> sont rapides et comparables, comme prévu.

Pour List<T>, utiliser le devant et le bout a tellement de différences! Et à ma grande surprise, les performances d'ajout/suppression de la fin sont en fait comparables à celles de Stack<T>.

Pour LinkedList<T>, la manipulation avec l'avant est rapide (-er que List<T>), mais à la fin, il est incroyablement lent pour enlever manipuler avec la fin est aussi.


Alors ... tout expert peut-il rendre compte de:

  1. la similitude dans l'exécution de l'utilisation de Stack<T> et de la fin de List<T>,
  2. les différences d'utilisation du début et de la fin de List<T>, et
  3. la raison pour laquelle utiliser la fin de LinkedList<T> est so slow (non applicable car il s'agit d'une erreur de codage due à l'utilisation de Last(), de Linq grâce à CodesInChaos )?

Je pense que je sais pourquoi List<T> ne gère pas si bien le recto ... parce que List<T> doit déplacer la liste entière dans les deux sens. Corrigez-moi si je me trompe.

P.S. Mon System.Diagnostics.Stopwatch.Frequency est 2435947 et le programme est destiné au profil de client .NET 4 et compilé avec C # 4.0 sur Windows 7 Visual Studio 2010.

50
Alvin Wong

Concernant 1:

Les performances de Stack<T> et de List<T> ne sont pas surprenantes. Je m'attendrais à ce que les deux utilisent des tableaux avec une stratégie de doublage. Cela conduit à des ajouts à temps constant amortis.

Vous pouvez utiliser List<T> partout où vous pouvez utiliser Stack<T>, mais cela conduit à un code moins expressif .

Concernant 2:

Je pense que je sais pourquoi List<T> ne gère pas si bien le recto ... parce que List<T> a besoin de déplacer toute la liste pour le faire.

C'est correct. L'insertion/suppression d'éléments au début est coûteuse car elle déplace tous les éléments. Obtenir ou remplacer des éléments au début n’est pas cher.

Concernant 3:

Votre valeur LinkedList<T>.RemoveLast lente est une erreur dans votre code d'analyse comparative.

Supprimer ou obtenir le dernier élément d'une liste doublement chaînée est peu coûteux. Dans le cas de LinkedList<T>, cela signifie que RemoveLast et Last sont bon marché.

Mais vous n'utilisiez pas la propriété Last, mais la méthode d'extension de LINQ Last(). Sur les collections qui n'implémentent pas IList<T>, il itère la liste entière en lui donnant l'exécution O(n).

35
CodesInChaos

List<T> est un tableau dynamique de surallocation (structure de données que vous verrez également dans la bibliothèque standard de nombreux autres langages). Cela signifie qu’il utilise en interne un tableau "statique" (un tableau ne pouvant pas être redimensionné, appelé simplement "tableau" dans .NET) qui peut être et est souvent plus grand que la taille de la liste. Ajouter ensuite incrémente simplement un compteur et utilise le prochain emplacement du module interne, précédemment inutilisé. Le tableau n'est réaffecté (ce qui nécessite la copie de tous les éléments) si le tableau interne devient trop petit pour accueillir tous les éléments. Lorsque cela se produit, la taille du tableau est augmentée d'un facteur (et non d'une constante), généralement 2.

Cela garantit que la durée de temps amortie (le temps moyen par opération sur une longue séquence d'opérations) pour l'ajout est de O(1), même dans le pire des cas. Pour l’ajout au début, aucune optimisation de ce type n’est réalisable (du moins pas en gardant à la fois l’accès aléatoire et l’ajout de O(1) à la fin). Il doit toujours copier tous les éléments pour les déplacer dans leurs nouveaux emplacements (en laissant de la place pour l'élément ajouté dans le premier emplacement). Stack<T> _ { fait la même chose } _, vous ne remarquez tout simplement pas la différence d'ajout à l'avant car vous n'opérez que sur une extrémité (la plus rapide).

Obtenir la fin d'une liste chaînée dépend beaucoup des éléments internes de votre liste. Un peut conserver une référence au dernier élément, mais cela rend toutes les opérations de la liste plus compliquées et peut (je n'ai pas d'exemple en main) rendre certaines opérations beaucoup plus coûteuses. En l'absence d'une telle référence, ajouter à la fin nécessite de parcourir les éléments all de la liste liée pour trouver le dernier nœud, ce qui est bien sûr extrêmement lent pour les listes de taille non primordiale.

Comme l'a souligné @CodesInChaos, la manipulation de votre liste liée était imparfaite. La récupération rapide de la fin que vous voyez maintenant est très probablement due au fait que LinkedList<T> conserve explicitement une référence au dernier nœud, comme mentionné ci-dessus. Notez que l'obtention d'un élément non à l'une ou l'autre extrémité est toujours lente.

13
user395760

La vitesse provient essentiellement du nombre d'opérations nécessaires pour insérer, supprimer ou rechercher un élément. Vous avez déjà remarqué que cette liste nécessite des transferts de mémoire. 

Stack est une liste accessible uniquement par l’élément supérieur - et l’ordinateur sait toujours où il se trouve.

La liste chaînée est une autre chose: le début de la liste est connu, il est donc très rapide d’ajouter ou de supprimer du début - mais trouver le dernier élément prend du temps. La mise en cache de l’emplacement du dernier élément OTOH n’est utile que pour l’ajout. Pour la suppression, il faut parcourir la liste complète moins un élément pour trouver le "crochet" ou le pointeur sur le dernier.

En regardant simplement les chiffres, on peut faire des suppositions éclairées des éléments internes de chaque structure de données:

  • pop d'une pile est rapide, comme prévu
  • Pousser pour empiler est plus lent. et c'est plus lent que d'ajouter à la fin de la liste. Pourquoi?
    • apparemment, la taille de l'unité d'allocation pour la pile est plus petite - cela peut seulement augmenter la taille de la pile de 100, alors que l'agrandissement de la liste pourrait se faire par unités de 1 000.
  • Une liste semble être un tableau statique. L'accès à la liste à l'avant nécessite des transferts de mémoire, ce qui prend du temps proportionnellement à la longueur de la liste.
  • Les opérations de base d'une liste chaînée ne devraient pas prendre beaucoup plus de temps, elles sont généralement nécessaires pour
    • new_item.next = list_start; list_start = new_item; // ajouter
    • list_start = list_start.next; // retirer
    • cependant, comme addLast est si rapide, cela signifie que même lors de l'ajout ou de la suppression d'une liste chaînée, il faut également mettre à jour le pointeur sur le dernier élément. Donc, il y a une comptabilité supplémentaire.
  • Les listes doublement chaînées OTOH rendent relativement rapide l'insertion et la suppression aux deux extrémités de la liste (j'ai appris qu'un meilleur code utilise des DLL), cependant.
    • les liens vers les articles précédents et suivants doublent également le travail de comptabilité
5
Aki Suihkonen

la similitude des performances d'utilisation de Stack et de la fin de la liste,

Comme expliqué par delnan, ils utilisent tous les deux un tableau simple en interne, ils se comportent donc de manière très similaire à la fin. Vous pourriez voir une pile être une liste avec juste accès au dernier objet.

les différences dans l'utilisation du début et de la fin de la liste

Vous l'avez déjà soupçonné correctement. Manipuler le début d'une liste signifie que le tableau sous-jacent doit être modifié. L'ajout d'un élément signifie généralement que vous devez déplacer tous les autres éléments d'un élément, comme pour la suppression. Si vous savez que vous manipulerez les deux extrémités d’une liste, vous feriez mieux d’utiliser une liste chaînée.

la raison pour laquelle l'utilisation de la fin de LinkedList est si lente?

En règle générale, l'insertion et la suppression d'éléments pour les listes chaînées à n'importe quelle position peuvent s'effectuer de manière constante, car il vous suffit de modifier au maximum deux pointeurs. Le problème est juste d'arriver à la position. Une liste liée normale a juste un pointeur sur son premier élément. Donc, si vous voulez atteindre le dernier élément, vous devez parcourir tous les éléments. Une file d'attente implémentée avec une liste chaînée résout généralement ce problème en ajoutant un pointeur sur le dernier élément, de sorte que l'ajout d'éléments est également possible en temps constant. La structure de données la plus sophistiquée serait une liste à double liaison qui comporte les deux pointeurs vers le premier et le dernier élément, et où chaque élément contient également un pointeur sur le prochain et le précédent élément.

Ce que vous devez apprendre à ce sujet, c’est qu’il existe de nombreuses structures de données conçues pour un seul but, qu’elles peuvent gérer très efficacement. Le choix de la structure dépend beaucoup de ce que vous voulez faire.

1
poke

J'ai une expérience en Java et je suppose que votre question concerne davantage les infrastructures de données générales qu'un langage spécifique. De plus, je m'excuse si mes déclarations sont incorrectes. 

1. la similitude des performances d'utilisation de Stack et de la fin de la liste

2. les différences d'utilisation entre le début et la fin de la liste et

Au moins en Java, les piles sont implémentées à l'aide de arrays (excuses si ce n'est pas le cas avec C #. Vous pouvez vous reporter au source pour l'implémentation). Il en va de même pour les listes. Typiquement avec un tableau, toutes les insertions à la fin prennent moins de temps qu'au début car les valeurs préexistantes du tableau doivent être abaissées pour permettre l'insertion au début. 

Lien vers le fichier source Stack.Java et sa superclasse Vector

3. la raison pour laquelle l'utilisation de la fin de LinkedList est si lente? 

LinkedList n'autorise pas l'accès aléatoire et doit traverser les nœuds avant d'atteindre votre point d'insertion. Si vous trouvez que les performances sont plus lentes pour les derniers nœuds, alors je suppose que l'implémentation de LinkedList devrait être une liste singly-linked. J'imagine que vous voudriez envisager une liste à double liaison pour des performances optimales tout en accédant aux éléments à la fin.

http://en.wikipedia.org/wiki/Linked_list

1
Arun Manivannan

Vient d’améliorer certaines des faiblesses du code précédent, notamment l’influence des calculs aléatoires et factices. Array domine toujours tout, mais les performances de List sont impressionnantes et LinkedList est très bon pour les insertions aléatoires.

Les résultats triés sont:

12      array[i]
40      list2[i]
62      FillArray
68      list2.RemoveAt
78      stack.Pop
126     list2.Add
127     queue.Dequeue
159     stack.Push
161     foreach_linkedlist1
191     queue.Enqueue
218     linkedlist1.RemoveFirst
219     linkedlist2.RemoveLast
2470        linkedlist2.AddLast
2940        linkedlist1.AddFirst

Le code est:

using System;
using System.Collections.Generic;
using System.Diagnostics;
//
namespace Benchmarking {
    //
    static class Collections {
        //
        public static void Main() {
            const int limit = 9000000;
            Stopwatch sw = new Stopwatch();
            Stack<int> stack = new Stack<int>();
            Queue<int> queue = new Queue<int>();
            List<int> list1 = new List<int>();
            List<int> list2 = new List<int>();
            LinkedList<int> linkedlist1 = new LinkedList<int>();
            LinkedList<int> linkedlist2 = new LinkedList<int>();
            int dummy;

            sw.Reset();
            Console.Write( "{0,40}  ", "stack.Push");
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                stack.Push( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "stack.Pop" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                stack.Pop();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "queue.Enqueue" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                queue.Enqueue( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "queue.Dequeue" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                queue.Dequeue();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            //sw.Reset();
            //Console.Write( "{0,40}  ", "Insert to List at the front..." );
            //sw.Start();
            //for ( int i = 0; i < limit; i++ ) {
            //  list1.Insert( 0, i );
            //}
            //sw.Stop();
            //Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            //
            //sw.Reset();
            //Console.Write( "{0,40}  ", "RemoveAt from List at the front..." );
            //sw.Start();
            //for ( int i = 0; i < limit; i++ ) {
            //  dummy = list1[ 0 ];
            //  list1.RemoveAt( 0 );
            //  dummy++;
            //}
            //sw.Stop();
            //Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            sw.Reset();
            Console.Write( "{0,40}  ", "list2.Add" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                list2.Add( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "list2.RemoveAt" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                list2.RemoveAt( list2.Count - 1 );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist1.AddFirst" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.AddFirst( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist1.RemoveFirst" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.RemoveFirst();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist2.AddLast" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist2.AddLast( i );
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );
            sw.Reset();
            Console.Write( "{0,40}  ", "linkedlist2.RemoveLast" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                linkedlist2.RemoveLast();
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill again
            for ( int i = 0; i < limit; i++ ) {
                list2.Add( i );
            }
            sw.Reset();
            Console.Write( "{0,40}  ", "list2[i]" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                dummy = list2[ i ];
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill array
            sw.Reset();
            Console.Write( "{0,40}  ", "FillArray" );
            sw.Start();
            var array = new int[ limit ];
            for ( int i = 0; i < limit; i++ ) {
                array[ i ] = i;
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            sw.Reset();
            Console.Write( "{0,40}  ", "array[i]" );
            sw.Start();
            for ( int i = 0; i < limit; i++ ) {
                dummy = array[ i ];
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );


            // Fill again
            for ( int i = 0; i < limit; i++ ) {
                linkedlist1.AddFirst( i );
            }
            sw.Reset();
            Console.Write( "{0,40}  ", "foreach_linkedlist1" );
            sw.Start();
            foreach ( var item in linkedlist1 ) {
                dummy = item;
            }
            sw.Stop();
            Console.WriteLine( sw.ElapsedMilliseconds.ToString() );

            //
            Console.WriteLine( "Press Enter to end." );
            Console.ReadLine();
        }
    }
}
0
cskwg