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:
Stack<T>
et de la fin de List<T>
,List<T>
, etLinkedList<T>
est so slowLast()
, 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.
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 queList<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)
.
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.
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:
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.
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.
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();
}
}
}