J'ai exécuté 3 expériences différentes impliquant des listes et des vecteurs C ++.
Ceux avec des vecteurs se sont révélés plus efficaces, même lorsque de nombreuses insertions au milieu étaient impliquées.
D'où la question: dans quel cas les listes ont-elles plus de sens que les vecteurs?
Si les vecteurs semblent plus efficaces dans la plupart des cas, et compte tenu de la ressemblance de leurs membres, quels avantages restent-ils pour les listes?
Générez N entiers et placez-les dans un conteneur afin que le conteneur reste trié. L'insertion a été effectuée naïvement, en lisant les éléments un par un et en insérant le nouveau juste avant le premier plus grand.
Avec une liste, le temps passe par le toit lorsque la dimension augmente, par rapport aux vecteurs.Insérez N entiers à la fin du conteneur.
Pour les listes et les vecteurs, le temps a augmenté du même ordre de grandeur, bien qu'il ait été 3 fois plus rapide avec les vecteurs.Insérez N entiers dans un conteneur.
Démarrer la minuterie.
Triez le conteneur en utilisant list.sort pour les listes et std :: sort pour les vecteurs. Arrêtez la minuterie.
Encore une fois, le temps augmente du même ordre de grandeur, mais il est en moyenne 5 fois plus rapide avec les vecteurs.
Je pourrais continuer à effectuer des tests et à trouver quelques exemples où les listes s'avéreraient meilleures.
Mais l'expérience commune de vous qui lisez ce message pourrait fournir des réponses plus productives.
Vous avez peut-être rencontré des situations où les listes étaient plus faciles à utiliser ou plus performantes?
la source
list
probablement mieux si vous supprimez beaucoup d'éléments. Je ne crois pas qu'unevector
volonté rendra la mémoire au système jusqu'à ce que le vecteur entier soit supprimé. Gardez également à l'esprit que votre test n ° 1 ne teste pas le temps d'insertion seul. C'est un test combinant recherche et insertion. C'est de trouver l'endroit où insérer oùlist
est lent. L'insertion réelle sera plus rapide que le vecteur.Réponses:
La réponse courte est que les cas semblent rares. Il y en a probablement quelques-uns cependant.
La première serait que vous ayez besoin de stocker un petit nombre de gros objets - en particulier, des objets si grands qu'il est impossible d'allouer de l'espace pour quelques-uns d'entre eux en plus. Il n'y a fondamentalement aucun moyen d'empêcher un vecteur ou deque d'allouer de l'espace pour des objets supplémentaires - c'est ainsi qu'ils sont définis (c'est-à-dire qu'ils doivent allouer de l'espace supplémentaire pour répondre à leurs exigences de complexité). Si vous ne pouvez pas allouer cet espace supplémentaire, un
std::list
peut être le seul conteneur standard qui répond à vos besoins.Un autre serait quand / si vous stockez un itérateur vers un point "intéressant" dans une liste pendant une longue période de temps, et lorsque vous effectuez des insertions et / ou des suppressions, vous le faites (presque) toujours à partir d'un emplacement auquel vous avez déjà un itérateur, donc vous ne parcourez pas la liste pour arriver au point où vous allez faire l'insertion ou la suppression. Évidemment, la même chose s'applique si vous travaillez avec plus d'un emplacement, mais prévoyez toujours de stocker un itérateur à chaque endroit avec lequel vous travaillerez probablement, de sorte que vous manipulez le plus les emplacements que vous pouvez atteindre directement et que vous parcourez rarement la liste pour obtenir à ces endroits.
Pour un exemple du premier, considérez un navigateur Web. Il peut conserver une liste liée d'
Tab
objets, chaque objet tab représentant un onglet ouvert dans le navigateur. Chaque onglet peut contenir quelques dizaines de mégaoctets de données (en plus, surtout si quelque chose comme une vidéo est impliqué). Votre nombre typique d'onglets ouverts peut facilement être inférieur à une douzaine, et 100 est probablement proche de l'extrême supérieur.Pour un exemple de la seconde, considérons un traitement de texte qui stocke le texte sous forme de liste chaînée de chapitres, chacun pouvant contenir une liste chaînée de (disons) paragraphes. Lorsque l'utilisateur modifie, il va généralement trouver un endroit particulier où il va éditer, puis faire une bonne quantité de travail à cet endroit (ou à l'intérieur de ce paragraphe, de toute façon). Oui, ils vont passer d'un paragraphe à l'autre de temps en temps, mais dans la plupart des cas, ce sera un paragraphe près de l'endroit où ils travaillaient déjà.
De temps en temps (des choses comme la recherche globale et le remplacement), vous finissez par parcourir tous les éléments de toutes les listes, mais c'est assez rare, et même si vous le faites, vous allez probablement faire assez de travail pour rechercher un élément dans la liste, que le temps de parcourir la liste est presque sans conséquence.
Notez que dans un cas typique, cela est susceptible de correspondre également au premier critère - un chapitre contient un nombre assez petit de paragraphes, dont chacun est susceptible d'être assez grand (au moins par rapport à la taille des pointeurs dans le nœud, etc.). De même, vous avez un nombre relativement faible de chapitres, chacun pouvant être de plusieurs kilo-octets environ.
Cela dit, je dois admettre que ces deux exemples sont probablement un peu artificiels, et même si une liste chaînée pourrait parfaitement fonctionner pour l'un ou l'autre, elle ne fournirait probablement pas un énorme avantage dans les deux cas également. Dans les deux cas, par exemple, l'allocation d'espace supplémentaire dans un vecteur pour certaines pages / onglets Web (vides) ou certains chapitres vides ne devrait pas poser de problème réel.
la source
std::vector
de pointeurs sera plus efficace que tous ces objets de noeud de liste liée.std::vector<std::unique_ptr<T>>
pourrait être une bonne alternative.Selon Bjarne Stroustrup lui-même, les vecteurs devraient toujours être la collection par défaut pour les séquences de données. Vous pouvez choisir la liste si vous souhaitez optimiser pour l'insertion et la suppression d'éléments, mais normalement vous ne devriez pas. Les coûts de la liste sont la traversée lente et l'utilisation de la mémoire.
Il en parle dans cette présentation .
Vers 0:44, il parle de vecteurs vs listes en général.
Vers 1 h 08, on lui pose une question à ce sujet.
la source
Le seul endroit où j'utilise habituellement des listes est l'endroit où j'ai besoin d'effacer des éléments et de ne pas invalider les itérateurs.
std::vector
invalide tous les itérateurs lors de l'insertion et de l'effacement.std::list
garantit que les itérateurs des éléments existants sont toujours valides après insertion ou suppression.la source
En plus des autres réponses déjà fournies, les listes ont certaines fonctionnalités qui n'existent pas dans les vecteurs (car elles seraient incroyablement chères). Les opérations d'épissage et de fusion étant les plus importantes. Si vous avez fréquemment un tas de listes qui doivent être ajoutées ou fusionnées, une liste est probablement un bon choix.
Mais si vous n'avez pas besoin d'effectuer ces opérations, alors probablement pas.
la source
Le manque de cache inhérent / convivialité des pages des listes liées a tendance à les faire presque entièrement rejetées par de nombreux développeurs C ++, et avec une bonne justification sous cette forme par défaut.
Les listes liées peuvent toujours être merveilleuses
Pourtant, les listes chaînées peuvent être merveilleuses lorsqu'elles sont soutenues par un allocateur fixe qui leur redonne cette localité spatiale qui leur manque intrinsèquement.
Là où ils excellent, c'est que nous pouvons diviser une liste en deux listes, par exemple, simplement en stockant un nouveau pointeur et en manipulant un ou deux pointeurs. Nous pouvons déplacer des nœuds d'une liste à une autre en temps constant par simple manipulation de pointeur, et une liste vide peut simplement avoir le coût en mémoire d'un seul
head
pointeur.Accélérateur de grille simple
À titre d'exemple pratique, considérons une simulation visuelle 2D. Il a un écran de défilement avec une carte qui s'étend sur 400x400 (160 000 cellules de la grille) utilisé pour accélérer des choses comme la détection de collision entre des millions de particules se déplaçant à chaque image (nous évitons ici les quadruples car ils ont tendance à avoir de moins bonnes performances avec ce niveau de données dynamiques). Un tas de particules se déplacent constamment à chaque image, ce qui signifie qu'elles passent constamment d'une cellule de grille à une autre.
Dans ce cas, si chaque particule est un nœud de liste à liaison unique, chaque cellule de la grille peut commencer comme un simple
head
pointeur pointant versnullptr
. Lorsqu'une nouvelle particule est née, nous la plaçons simplement dans la cellule de grille où elle se trouve en définissant lehead
pointeur de cette cellule pour pointer vers ce nœud de particule. Lorsqu'une particule se déplace d'une cellule de grille à la suivante, nous manipulons simplement des pointeurs.Cela peut être beaucoup plus efficace que de stocker 160 000
vectors
pour chaque cellule de la grille et de repousser et d'effacer tout le temps du milieu image par image.std :: list
Il s'agit cependant de listes roulées à la main, intrusives et liées individuellement, soutenues par un allocateur fixe.
std::list
représente une liste doublement liée et elle peut ne pas être aussi compacte lorsqu'elle est vide qu'un seul pointeur (varie selon l'implémentation du fournisseur), en plus c'est un peu difficile d'implémenter des allocateurs personnalisés sousstd::allocator
forme.Je dois admettre que je n'en utilise jamais
list
. Mais les listes chaînées peuvent toujours être merveilleuses! Pourtant, ils ne sont pas merveilleux pour les raisons pour lesquelles les gens sont souvent tentés de les utiliser, et pas si merveilleux à moins qu'ils ne soient soutenus par un allocateur fixe très efficace qui atténue au moins les défauts de page obligatoires et les manquements de cache associés.la source
std::forward_list
.Vous devez considérer la taille des éléments dans le conteneur.
int
Le vecteur d'éléments est très rapide car la plupart des données tiennent dans le cache CPU (et les instructions SIMD peuvent probablement être utilisées pour la copie de données).Si la taille de l'élément est supérieure, le résultat des tests 1 et 3 pourrait changer considérablement.
D'une comparaison très complète des performances :
(comme une note annexe
std::deque
est une structure de données très sous-estimée).D'un point de vue pratique,
std::list
offre la garantie que les itérateurs ne deviennent jamais invalides lorsque vous insérez et supprimez d'autres éléments. C'est souvent un aspect clé.la source
À mon avis, la raison la plus importante d'utiliser des listes est l' invalidation des itérateurs : si vous ajoutez / supprimez des éléments à un vecteur, tous les pointeurs, références, itérateurs que vous déteniez sur des éléments particuliers de ce vecteur peuvent être invalidés et conduire à des bogues subtils. ou défauts de segmentation.
Ce n'est pas le cas des listes.
Les règles précises pour tous les conteneurs standard sont données dans ce post StackOverflow .
la source
Bref, il n'y a pas de bonne raison d'utiliser
std::list<>
:Si vous avez besoin d'un conteneur non trié,
std::vector<>
règles.(Supprimez les éléments en les remplaçant par le dernier élément du vecteur.)
Si vous avez besoin d'un conteneur trié,
std::vector<shared_ptr<>>
règles.Si vous avez besoin d'un index clairsemé, des
std::unordered_map<>
règles.C'est ça.
Je trouve qu'il n'y a qu'une seule situation où j'ai tendance à utiliser une liste chaînée: lorsque j'ai des objets préexistants qui doivent être connectés d'une manière ou d'une autre pour implémenter une logique d'application supplémentaire. Cependant, dans ce cas, je n'utilise jamais
std::list<>
, je préfère recourir à un pointeur suivant (intelligent) à l'intérieur de l'objet, d'autant plus que la plupart des cas d'utilisation aboutissent à un arbre plutôt qu'à une liste linéaire. Dans certains cas, la structure résultante est une liste chaînée, dans d'autres, c'est un arbre ou un graphe acyclique dirigé. Le but principal de ces pointeurs est toujours de construire une structure logique, jamais de gérer des objets. Nous avonsstd::vector<>
pour ça.la source
Vous devez montrer comment vous faisiez les inserts lors de votre premier test. Vos deuxième et troisième tests, vector gagneront facilement.
Une utilisation importante des listes est lorsque vous devez prendre en charge la suppression d'éléments lors de l'itération. Lorsque le vecteur est modifié, tous les itérateurs sont (potentiellement) invalides. Avec une liste, seul un itérateur de l'élément supprimé n'est pas valide. Tous les autres itérateurs restent valides.
L'ordre d'utilisation typique des conteneurs est vector, deque, puis list. Le choix du conteneur est généralement basé sur push_back choose vector, pop_front choose deque, insert choose list.
la source
Un facteur auquel je peux penser est qu'à mesure qu'un vecteur grandit, la mémoire libre se fragmente à mesure que le vecteur désalloue sa mémoire et alloue un bloc plus grand encore et encore. Ce ne sera pas un problème avec les listes.
Ceci s'ajoute au fait qu'un grand nombre de
push_back
s sans réserve provoquera également une copie lors de chaque redimensionnement ce qui le rend inefficace. L'insertion au milieu provoque également un déplacement de tous les éléments vers la droite, et est encore pire.Je ne sais pas si c'est une préoccupation majeure, mais c'est la raison qui m'a été donnée lors de mon travail (développement de jeux mobiles), pour éviter les vecteurs.
la source