66 votes

Les différences de performance... si spectaculaires ?

Je viens de lire quelques posts sur List<T> vs LinkedList<T> J'ai donc décidé d'évaluer moi-même certaines structures. J'ai évalué Stack<T> , Queue<T> , List<T> y LinkedList<T> en ajoutant des données et en supprimant des données de/vers le front/end. Voici le résultat du benchmark :

               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 les suivantes donc dramatique !

Comme vous pouvez le constater, les performances de Stack<T> y Queue<T> sont rapides et comparables, c'est normal.

Para List<T> l'utilisation de l'avant et de l'arrière a tellement de différences ! Et à ma grande surprise, la performance de l'ajout/suppression à partir de la fin est en fait comparable à la performance de l'ajout/suppression à partir de la fin. Stack<T> .

Para LinkedList<T> la manipulation avec l'avant est rapide (-er que List<T> ) mais en fin de compte, il est incroyablement lent à retirer. manipuler avec la fin est aussi.


Alors... des experts peuvent-ils nous dire.. :

  1. la similitude des performances de l'utilisation Stack<T> et la fin de List<T> ,
  2. les différences dans l'utilisation de l'avant et de l'extrémité de la List<T> et
  3. la raison pour laquelle l'utilisation de la fin de LinkedList<T> est donc lent (sans objet, car il s'agit d'une erreur de codage due à l'utilisation de la méthode de Linq Last() , Merci à CodesInChaos ) ?

Je pense que je sais pourquoi List<T> ne gère pas si bien le front... parce que List<T> doit déplacer la liste entière d'avant en arrière quand il fait ça. Corrigez-moi si je me trompe.

P.S. Mon System.Diagnostics.Stopwatch.Frequency est 2435947 Le programme est conçu pour le profil client .NET 4 et compilé avec C# 4.0, sur Windows 7 Visual Studio 2010.

9 votes

Sans vouloir vous offenser, vous devriez d'abord apprendre ce que sont ces structures de données et quelle est la complexité (notation du grand O) de chaque opération sur chaque structure de données. Malheureusement, MSDN ne vous aidera pas dans ce domaine, mais il ne devrait pas être difficile de trouver suffisamment d'informations sur Wikipedia.

6 votes

@EmperorOrionii Oui, je comprends ces structures. J'étais juste perplexe sur la raison pour laquelle une liste à double lien prend autant de temps pour accéder à la fin mais il s'est avéré que c'était une erreur de codage.

43voto

CodesInChaos Points 60274

Concernant 1 :

Stack<T> et List<T> Il n'est pas surprenant que les performances de l'entreprise soient similaires. Je m'attendais à ce que les deux utilisent des tableaux avec une stratégie de doublement. Cela conduit à des ajouts amortis en temps constant.

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 front... parce que List<T> doit déplacer toute la liste d'avant en arrière quand il fait ça.

C'est exact. Insérer/retirer des éléments au début est coûteux car cela déplace tous les éléments. Par contre, récupérer ou remplacer des éléments au début est bon marché.

Concernant 3 :

Votre lenteur LinkedList<T>.RemoveLast est une erreur dans votre code de référence.

Retirer ou récupérer le dernier élément d'une liste doublement liée est peu coûteux. Dans le cas de LinkedList<T> cela signifie que RemoveLast y Last sont bon marché.

Mais vous n'utilisiez pas le 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 O(n) temps d'exécution.

2 votes

Oh, tu as raison, changer pour LinkedList<T>.Last.Value est beaucoup mieux !

1 votes

+1 Je n'ai pas vérifié le code et je n'en savais pas assez sur le sujet. LinkedList<T> trouve le résultat suspect.

0 votes

J'ai finalement décidé de l'accepter car tu es le seul à avoir trouvé mon code buggué en LinkedList<T> .

13voto

delnan Points 52260

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

Cela permet de garantir que amorti La complexité temporelle (en gros, le temps moyen par opération sur une longue séquence d'opérations) pour l'ajout est O(1) même dans le pire des cas. Pour l'ajout à l'avant, aucune optimisation de ce type n'est réalisable (du moins pas en conservant à la fois l'accès aléatoire et l'ajout O(1) à la fin). Il faut toujours copier tous les éléments pour les déplacer dans leurs nouveaux emplacements (en faisant de la place pour l'élément ajouté dans le premier emplacement). Stack<T> fait la même chose Si l'on ajoute des éléments à l'avant, on ne remarque pas l'écart parce qu'on ne travaille que sur une extrémité (la plus rapide).

Obtenir la fin d'une liste chaînée dépend beaucoup de la structure interne de votre liste. Un site peut maintenir une référence au dernier élément, mais cela rend toutes les opérations sur la liste plus compliquées, et peut (je n'ai pas d'exemple sous la main) rendre certaines opérations beaucoup plus coûteuses. En l'absence d'une telle référence, l'ajout d'un élément à la fin de la liste nécessite de passer par les étapes suivantes todo éléments de la liste liée pour trouver le dernier nœud, ce qui est bien sûr terriblement lent pour les listes de taille non triviale.

Comme l'a souligné @CodesInChaos, votre manipulation de la liste chaînée était défectueuse. La récupération rapide de la fin que vous voyez maintenant est très probablement causée par LinkedList<T> en maintenant explicitement une référence au dernier nœud, comme mentionné ci-dessus. Notez que l'obtention d'un élément qui ne se trouve pas à l'une ou l'autre extrémité est toujours lente.

0 votes

"Pour l'ajout à l'avant, une telle optimisation n'est pas réalisable." Je suppose que vous pourriez placer les éléments de la liste au milieu du tableau "statique", et conserver un décalage par rapport à l'avant, au lieu d'utiliser un décalage de 0. Ensuite, vous pourriez ajouter des éléments à la liste en temps constant en utilisant la position (offset-1) puis en décrémentant l'offset. Je suppose que cela ne vaut pas la peine de gaspiller la mémoire pour un cas aussi spécial.

1 votes

@smackfu Je suppose que cela pourrait fonctionner, mais je ne suis pas sûr de la façon dont cela interagit avec le fait de faire la même chose à la fin, et si cela finit par "juste" économiser un grand facteur constant ou si cela améliorerait la complexité amortie. Et oui, ce serait un cas assez spécial. Une deque est meilleure pour les opérations aux deux extrémités.

0 votes

@smackfu Même que l'insertion au milieu serait toujours O(n). On pourrait faire des optimisations au cas par cas mais cela compliquerait très vite les choses. Mais il existe quelque chose dans ce sens, une liste d'arbres où les éléments sont stockés comme des feuilles dans un arbre auto-équilibré. C'est un peu plus lent en accès aléatoire que la liste basée sur un tableau, mais sa complexité est de O(log(n)) pour l'insertion et la suppression (et l'accès) d'un élément à un indice aléatoire. J'ai trouvé l'implémentation pour Java mais je n'ai pas eu de chance avec la solution .Net.

6voto

Aki Suihkonen Points 9888

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

La pile est une liste, qui n'est accessible que 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 retirer du début - mais trouver le dernier élément prend du temps. La mise en cache de l'emplacement du dernier élément ne vaut 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 vers le dernier élément.

En regardant simplement les chiffres, on peut faire quelques suppositions sur les caractéristiques internes de chaque structure de données :

  • le pop depuis une pile est rapide, comme prévu
  • Pousser vers la pile 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 -- elle peut seulement augmenter la taille de la pile de 100, alors que la croissance de la liste pourrait être faite en unités de 1000.
  • Une liste semble être un tableau statique. Accéder à la liste à l'avant nécessite des transferts de mémoire, qui prennent du temps en proportion de la longueur de la liste.
  • Les opérations de base sur les listes chaînées ne devraient pas prendre beaucoup plus de temps, il suffit généralement d'effectuer les opérations suivantes
    • new_item.next = list_start ; list_start = new_item ; // pour ajouter
    • list_start = list_start.next ; // à supprimer
    • Cependant, comme addLast est si rapide, cela signifie que lors d'un ajout ou d'une suppression dans une liste liée, il faut également mettre à jour le pointeur du dernier élément. Il y a donc une comptabilité supplémentaire.
  • Les listes doublement liées permettent en revanche d'insérer et d'effacer relativement rapidement aux deux extrémités de la liste (j'ai été informé qu'un meilleur code utilise les DLL),
    • Les liens vers l'article précédent et suivant doublent également le travail de comptabilité.

0 votes

(1) La réallocation pour List et Stack se fait (très probablement) en multipliant la taille par une petite constante, et non en ajoutant une constante. L'ajout d'une constante est inflexible : Il entraîne plus de réallocations pour les grandes collections et gaspille plus d'espace pour les petites collections. (2) Votre pseudo-code pour les listes liées suppose une liste à liaison simple, mais c'est une liste à liaison double.

1 votes

@delnan : Oui, pour être précis. Certaines stratégies plafonnent le facteur d'augmentation, d'autres utilisent des séries de fibonacci. Je dois encore supposer que les stratégies diffèrent pour les piles et les files d'attente -- peut-être en supposant que les files d'attente sont utilisées pour les "grosses" données et les piles pour les "petites" données et que le choix de conception des piles est fait pour minimiser la mémoire inutilisée.

1voto

Arun Manivannan Points 1596

J'ai une formation en Java et je pense que votre question concerne davantage les structures de données générales qu'un langage spécifique. Aussi, je m'excuse si mes déclarations sont incorrectes.

1. la similitude des performances de l'utilisation de Stack et de la fin de List

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

Au moins en Java, les piles sont mises en œuvre à l'aide des éléments suivants tableaux (Toutes mes excuses si ce n'est pas le cas avec C#. Vous pouvez vous référer à la source pour l'implémentation) Et 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 parce que les valeurs préexistantes dans le tableau doivent être déplacées vers le bas pour accommoder l'insertion au début.

Lien vers la source de Stack.java et sa superclasse Vecteur

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

LinkedList ne permettent pas l'accès aléatoire et vous devez 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 la LinkedList devrait être une liste à lien unique . Je pense qu'il faudrait envisager une liste à double lien pour des performances optimales lors de l'accès aux éléments à la fin.

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

1voto

poke Points 64398

la similitude des performances de l'utilisation de Stack et de la fin de List,

Comme l'a expliqué delnan, ils utilisent tous deux un tableau simple en interne, et se comportent donc de manière très similaire lorsqu'ils travaillent à la fin. On pourrait voir une pile comme une liste avec juste un 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écaler tous les autres éléments d'une unité, de même que la suppression. Si vous savez que vous allez manipuler les deux extrémités d'une liste, il est préférable d'utiliser une liste chaînée.

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

En général, l'insertion et la suppression d'éléments pour les listes liées à n'importe quelle position peuvent être effectuées en temps constant, car il suffit de changer au maximum deux pointeurs. Le problème est d'arriver à la position. Une liste chaînée normale n'a qu'un pointeur sur son premier élément. Donc, si vous voulez atteindre le dernier élément, vous devez itérer à travers 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 ayant un pointeur supplémentaire 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 doublement liée qui possède à la fois des pointeurs vers le premier élément et vers le dernier élément. et enfin et où chaque élément contient également un pointeur vers l'élément suivant. et précédents élément.

Ce que vous devez savoir à ce sujet, c'est qu'il existe de nombreuses structures de données différentes, conçues dans un seul but, qu'elles peuvent traiter très efficacement. Le choix de la bonne structure dépend beaucoup de ce que vous voulez faire.

0 votes

LinkedList est une liste doublement liée. Le message original contenait une erreur.

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