web-dev-qa-db-fra.com

Quel est l'intérêt d'utiliser des listes sur des vecteurs, en C ++?

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?

  1. 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.

  2. 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.

  3. 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?

32
Marek Stanley

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 d'objets volumineux, 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 arrêtez ne peut pas autoriser l'allocation de cet espace supplémentaire, un std::list peut être le niquement 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 chaînée d'objets Tab, 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 assez petit nombre 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 relativement petit nombre 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.

35
Jerry Coffin

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 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.

La compacité est importante. Les vecteurs sont plus compacts que les listes. Et les modèles d'utilisation prévisibles sont extrêmement importants. Avec les vecteurs, vous devez repousser beaucoup d'éléments, mais les caches sont vraiment très bons dans ce domaine. ... Les listes n'ont pas d'accès aléatoire. Mais lorsque vous parcourez une liste, vous continuez à faire un accès aléatoire. Il y a un nœud ici, et il va à ce nœud, en mémoire. Ainsi, vous accédez en fait de manière aléatoire à votre mémoire et vous maximisez vos échecs de cache, ce qui est exactement le contraire de ce que vous voulez.

Vers 1 h 08, on lui pose une question à ce sujet.

Ce que nous devrions voir, c'est que nous avons besoin d'une séquence d'éléments. Et la séquence d'éléments par défaut en C++ est le vecteur. Maintenant, parce que c'est compact et efficace. L'implémentation, le mappage au matériel, compte. Maintenant, si vous souhaitez optimiser l'insertion et la suppression - vous dites: "Eh bien, je ne veux pas la version par défaut d'une séquence. Je veux celui spécialisé, qui est une liste ". Et si vous faites cela, vous devriez en savoir suffisamment pour dire: "J'accepte certains coûts et certains problèmes, comme des traversées lentes et une utilisation accrue de la mémoire".

26
Pete

Le seul endroit où j'utilise habituellement des listes est celui où j'ai besoin d'effacer les éléments et non d'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.

11
UldisK

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.

4
David C.

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 la localité spatiale qui leur manque intrinsèquement.

Là où ils Excel, 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 pointeur head.

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 à l'autre.

Dans ce cas, si chaque particule est un nœud de liste à liaison unique, chaque cellule de grille peut commencer comme un simple pointeur head qui pointe vers nullptr. Lorsqu'une nouvelle particule est née, nous la plaçons simplement dans la cellule de grille où elle se trouve en définissant le pointeur head 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

C'est pour les 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), et c'est en quelque sorte un problème d'implémenter des allocateurs personnalisés dans std::allocator forme.

Je dois admettre que je n'utilise jamais list que ce soit. 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.

3
user204677

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'un comparaison très complète des performances :

Cela permet de tirer des conclusions simples sur l'utilisation de chaque structure de données:

  • Calcul des nombres: utilisez std::vector ou std::deque
  • Recherche linéaire: utilisez std::vector ou std::deque
  • Insertion/suppression aléatoire:
    • Petite taille de données: utilisez std::vector
    • Grande taille d'élément: utilisez std::list (sauf s'il est principalement destiné à la recherche)
  • Type de données non trivial: utilisez std::list sauf si vous avez besoin du conteneur spécialement pour la recherche. Mais pour de multiples modifications du conteneur, ce sera très lent.
  • Poussez vers l'avant: utilisez std::deque ou std::list

(comme note secondaire 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é.

2
manlio

À mon avis, la raison la plus importante d'utiliser des listes est l'invalidation de l'itérateur : si vous ajoutez/supprimez des éléments à un vecteur, tous les pointeurs, références, itérateurs qui vous vous êtes tenu à des éléments particuliers de ce vecteur peut être invalidé et conduire à des bugs subtils ... ou à des erreurs 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 .

2

En bref, il n'y a aucune bonne raison d'utiliser std::list<>:

  • Si vous avez besoin d'un conteneur non trié, std::vector<> règles.
    (Supprimer 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é, 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<>, j'ai plutôt recours à 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. On a std::vector<> pour ça.