web-dev-qa-db-fra.com

Liste de liens efficace en C ++?

Ce document dit std::list est inefficace:

std :: list est une classe extrêmement inefficace qui est rarement utile. Il effectue une allocation de tas pour chaque élément inséré, ayant ainsi un facteur constant extrêmement élevé, en particulier pour les petits types de données.

Commentaire: c'est à ma grande surprise. std::list est une liste doublement liée, donc malgré son inefficacité dans la construction des éléments, il prend en charge l'insertion/suppression dans la complexité temporelle O(1)), mais cette fonctionnalité est complètement ignorée dans ce paragraphe cité.

Ma question: Disons que j'ai besoin d'un conteneur séquentiel pour les petites tailles homogènes éléments, et ce conteneur doit prendre en charge l'élément insérer/supprimer dans la complexité O (1) et ne pas besoin d'un accès aléatoire (bien que la prise en charge de l'accès aléatoire soit agréable, ce n'est pas un must ici). Je ne veux pas non plus le facteur constant élevé introduit par l'allocation de tas pour la construction de chaque élément , au moins lorsque le nombre d'éléments est petit. Enfin, les itérateurs ne doivent être invalidés que lorsque l'élément correspondant est supprimé. Apparemment, j'ai besoin d'une classe de conteneur personnalisée, qui pourrait (ou non) être une variante de la liste doublement liée. Comment dois-je concevoir ce conteneur?

Si la spécification susmentionnée ne peut pas être atteinte, alors peut-être devrais-je avoir un allocateur de mémoire personnalisé, disons, un allocateur de pointeur de relief? Je sais std::list prend un allocateur comme deuxième argument de modèle.

Edit: Je sais que je ne devrais pas être trop préoccupé par ce problème, d'un point de vue technique - assez rapide est assez bon. C'est juste une question hypothétique donc je n'ai pas de cas d'utilisation plus détaillé. N'hésitez pas à assouplir certaines des exigences!

Edit2: je comprends deux algorithmes de [~ # ~] o [~ # ~] (1) la complexité peut avoir des performances entièrement différentes en raison de la différence de leurs facteurs constants.

42
Leedehai

La façon la plus simple que je vois pour répondre à toutes vos exigences:

  1. Insertion/suppression à temps constant (espérons que le temps constant amorti est correct pour l'insertion).
  2. Aucune allocation/désallocation de tas par élément.
  3. Aucune invalidation d'itérateur lors de la suppression.

... serait quelque chose comme ça, en utilisant simplement std::vector:

template <class T>
struct Node
{
    // Stores the memory for an instance of 'T'.
    // Use placement new to construct the object and
    // manually invoke its dtor as necessary.
    typename std::aligned_storage<sizeof(T), alignof(T)>::type element;

    // Points to the next element or the next free
    // element if this node has been removed.
    int next;

    // Points to the previous element.
    int prev;
};

template <class T>
class NodeIterator
{
public:
    ...
private:
    std::vector<Node<T>>* nodes;
    int index;
};

template <class T>
class Nodes
{
public:
    ...
private:
    // Stores all the nodes.
    std::vector<Node> nodes;

    // Points to the first free node or -1 if the free list
    // is empty. Initially this starts out as -1.
    int free_head;
};

... et j'espère avec un meilleur nom que Nodes (je suis un peu éméché et pas si bon à trouver des noms pour le moment). Je vous laisse l'implémentation mais c'est l'idée générale. Lorsque vous supprimez un élément, effectuez simplement une suppression de liste à double liaison en utilisant les index et poussez-le vers la tête libre. L'itérateur n'invalide pas car il stocke un index dans un vecteur. Lorsque vous insérez, vérifiez si la tête libre est -1. Sinon, écrasez le nœud à cette position et sautez. Autrement Push_back au vecteur.

Illustration

Diagramme (les nœuds sont stockés de manière contiguë dans std::vector, nous utilisons simplement des liens d'index pour permettre de sauter des éléments de manière sans branche avec des suppressions et des insertions en temps constant n'importe où):

enter image description here

Disons que nous voulons supprimer un nœud. C'est votre suppression de liste doublement liée standard, sauf que nous utilisons des index au lieu de pointeurs et que vous poussez également le nœud vers la liste libre (ce qui implique simplement la manipulation d'entiers):

Ajustement de la suppression des liens:

enter image description here

Pousser le nœud supprimé vers la liste libre:

enter image description here

Supposons maintenant que vous insériez dans cette liste. Dans ce cas, vous retirez la tête libre et écrasez le nœud à cette position.

Après l'insertion:

enter image description here

L'insertion au milieu en temps constant devrait également être facile à comprendre. Fondamentalement, vous insérez simplement dans la tête libre ou Push_back au vecteur si la pile libre est vide. Ensuite, vous faites votre insertion de liste double-link standard. Logique pour la liste gratuite (bien que j'ai fait ce diagramme pour quelqu'un d'autre et qu'il implique une SLL, mais vous devriez avoir l'idée):

enter image description here

Assurez-vous que vous construisez et détruisez correctement les éléments en plaçant des appels nouveaux et manuels au dtor lors de l'insertion/suppression. Si vous voulez vraiment le généraliser, vous devrez également penser à la sécurité des exceptions et nous avons également besoin d'un const itérateur en lecture seule.

Avantages et inconvénients

L'avantage d'une telle structure est qu'elle permet des insertions/suppressions très rapides de n'importe où dans la liste (même pour une liste gigantesque), l'ordre d'insertion est préservé pour la traversée, et il n'invalide jamais les itérateurs à l'élément qui n'est pas directement supprimé (bien que cela leur invalide les pointeurs; utilisez deque si vous ne voulez pas que les pointeurs soient invalidés). Personnellement, j'y trouverais plus d'utilité que std::list (que je n'utilise pratiquement jamais).

Pour des listes suffisamment grandes (disons, plus grandes que l'ensemble de votre cache L3 comme un cas où vous devriez certainement vous attendre à un énorme Edge), cela devrait largement surpasser std::vector pour les suppressions et les insertions vers/depuis le milieu et l'avant. La suppression d'éléments du vecteur peut être assez rapide pour les petits, mais essayez de supprimer un million d'éléments d'un vecteur en commençant par l'avant et en travaillant vers l'arrière. Là, les choses vont commencer à ramper pendant que celle-ci se termine en un clin d'œil. std::vector est toujours un peu sur-typé IMO lorsque les gens commencent à utiliser sa méthode erase pour supprimer des éléments du milieu d'un vecteur couvrant 10 000 éléments ou plus, bien que je suppose que cela soit toujours préférable à ceux qui utilisent naïvement des listes chaînées partout de manière à ce que chaque nœud soit alloué individuellement contre un allocateur à usage général tout en provoquant de nombreux manquements de cache.

L'inconvénient est qu'il ne prend en charge que l'accès séquentiel, nécessite la surcharge de deux entiers par élément, et comme vous pouvez le voir dans le diagramme ci-dessus, sa localité spatiale se dégrade si vous supprimez constamment des choses de façon sporadique.

Dégradation de la localisation spatiale

La perte de localité spatiale lorsque vous commencez à retirer et à insérer beaucoup de/vers le milieu entraînera des schémas d'accès à la mémoire en zigzag, potentiellement expulsant les données d'une ligne de cache uniquement pour revenir en arrière et les recharger pendant une seule boucle séquentielle. Cela est généralement inévitable avec toute structure de données qui permet des suppressions du milieu en temps constant tout en permettant également à cet espace d'être récupéré tout en préservant l'ordre d'insertion. Cependant, vous pouvez restaurer la localité spatiale en proposant une méthode ou vous pouvez copier/échanger la liste. Le constructeur de copie peut copier la liste d'une manière qui itère dans la liste source et insère tous les éléments, ce qui vous donne un vecteur parfaitement contigu, compatible avec le cache, sans trous (bien que cela invalide les itérateurs).

Alternative: attribution de liste gratuite

Une alternative qui répond à vos besoins est de mettre en place une liste gratuite conforme à std::allocator et l'utiliser avec std::list. Je n'ai jamais aimé atteindre les structures de données et jouer avec des allocateurs personnalisés et que l'on doublerait l'utilisation de la mémoire des liens sur 64 bits en utilisant des pointeurs au lieu d'index 32 bits, donc je préférerais personnellement la solution ci-dessus en utilisant std::vector comme essentiellement votre allocateur de mémoire analogique et vos index au lieu de pointeurs (qui à la fois réduisent la taille et deviennent une exigence si nous utilisons std::vector puisque les pointeurs seraient invalidés lorsque vector réservera une nouvelle capacité).

Listes indexées liées

J'appelle ce genre de chose une "liste liée indexée" car la liste liée n'est pas vraiment un conteneur autant qu'un moyen de relier ensemble des choses déjà stockées dans un tableau. Et je trouve ces listes liées indexées exponentiellement plus utiles car vous n'avez pas à vous mettre à genoux dans les pools de mémoire pour éviter les allocations/désallocations de tas par nœud et vous pouvez toujours conserver une localité de référence raisonnable (grand LOR si vous pouvez vous permettre de poster- traiter les choses ici et là pour restaurer la localité spatiale).

Vous pouvez également créer ce lien unique si vous ajoutez un entier supplémentaire à l'itérateur de nœud pour stocker l'index de nœud précédent (sans frais de mémoire sur 64 bits en supposant des exigences d'alignement 32 bits pour int et 64- peu pour les pointeurs). Cependant, vous perdez ensuite la possibilité d'ajouter un itérateur inversé et de rendre tous les itérateurs bidirectionnels.

Indice de référence

J'ai concocté une version rapide de ce qui précède, car vous semblez intéressé par 'em: build build, MSVC 2012, pas d'itérateurs vérifiés ou quelque chose comme ça:

--------------------------------------------
- test_vector_linked
--------------------------------------------
Inserting 200000 elements...
time passed for 'inserting': {0.000015 secs}

Erasing half the list...
time passed for 'erasing': {0.000021 secs}
time passed for 'iterating': {0.000002 secs}
time passed for 'copying': {0.000003 secs}

Results (up to 10 elements displayed):
[ 11 13 15 17 19 21 23 25 27 29 ]

finished test_vector_linked: {0.062000 secs}
--------------------------------------------
- test_vector
--------------------------------------------
Inserting 200000 elements...
time passed for 'inserting': {0.000012 secs}

Erasing half the vector...
time passed for 'erasing': {5.320000 secs}
time passed for 'iterating': {0.000000 secs}   
time passed for 'copying': {0.000000 secs}

Results (up to 10 elements displayed):
[ 11 13 15 17 19 21 23 25 27 29 ]

finished test_vector: {5.320000 secs}

Était trop paresseux pour utiliser une minuterie de haute précision, mais j'espère que cela donne une idée de pourquoi on ne devrait pas utiliser vector's temps linéaire erase méthode dans les chemins critiques pour les tailles d'entrée non triviales avec vector ci-dessus prenant ~ 86 fois plus longtemps (et exponentiellement pire plus la taille d'entrée est grande - j'ai essayé avec 2 millions d'éléments à l'origine mais ont abandonné après avoir attendu près de 10 minutes) et pourquoi je pense que vector est toujours très légèrement sur-typé pour ce type d'utilisation. Cela dit, nous pouvons transformer la suppression du milieu en une opération à temps constant très rapide sans mélanger l'ordre des éléments, sans invalider les index et les itérateurs les stockant, et tout en utilisant vector... Tout ce que nous avons faire est simplement de lui faire stocker un nœud lié avec prev/next index pour permettre de sauter les éléments supprimés.

Pour la suppression, j'ai utilisé un vecteur source aléatoire mélangé d'indices pairs pour déterminer quels éléments supprimer et dans quel ordre. Cela imite quelque peu un cas d'utilisation du monde réel où vous supprimez du milieu de ces conteneurs via des index/itérateurs que vous avez précédemment obtenus, comme la suppression des éléments que l'utilisateur sélectionnait auparavant avec un outil de sélection après qu'il est le bouton Supprimer (et encore une fois, vous ne devrait vraiment pas utiliser scalaire vector::erase pour cela avec des tailles non triviales; il serait même préférable de construire un ensemble d'indices pour supprimer et utiliser remove_if - toujours mieux que vector::erase a appelé un itérateur à la fois).

Notez que l'itération devient légèrement plus lente avec les nœuds liés, et cela n'a pas à voir avec la logique d'itération autant que le fait que chaque entrée dans le vecteur est plus grande avec les liens ajoutés (plus de mémoire à traiter séquentiellement équivaut à plus de cache échecs et défauts de page). Néanmoins, si vous faites des choses comme supprimer des éléments de très grandes entrées, le biais de performance est si épique pour les grands conteneurs entre la suppression en temps linéaire et en temps constant que cela a tendance à être un échange utile.

1
Dragon Energy

Vos exigences sont exactement celles de std::list, sauf que vous avez décidé que vous n'aimez pas la surcharge de l'allocation basée sur les nœuds.

L'approche sensée consiste à commencer par le haut et à ne faire que ce dont vous avez vraiment besoin:

  1. Utilisez simplement std::list.

    Comparez-le: l'allocateur par défaut est-il vraiment trop lent pour vos besoins?

    • Non: vous avez terminé.

    • Oui: goto 2

  2. Utilisation std::list avec un allocateur personnalisé existant tel que l'allocateur de pool Boost

    Comparez-le: l'allocateur de pool Boost est-il vraiment trop lent pour vos besoins?

    • Non: vous avez terminé.

    • Oui: goto 3

  3. Utilisation std::list avec un répartiteur personnalisé roulé à la main, parfaitement adapté à vos besoins uniques, sur la base de tout le profilage que vous avez effectué aux étapes 1 et 2

    Benchmark comme avant etc. etc.

  4. Envisagez de faire quelque chose de plus exotique en dernier recours.

    Si vous arrivez à ce stade, vous devriez avoir une question vraiment bien spécifiée SO, avec beaucoup de détails sur exactement ce dont vous avez besoin (par exemple. "Je dois presser n nœuds dans une cacheline "plutôt que" ce doc a dit que cette chose est lente et que ça sonne mal ").


PS. Ce qui précède fait deux hypothèses, mais les deux méritent d'être étudiées:

  1. comme le souligne Baum mit Augen, il ne suffit pas de faire un timing simple de bout en bout, car vous devez être sûr de savoir où va votre temps. Il peut s'agir de l'allocateur lui-même, ou du cache manqué en raison de la disposition de la mémoire, ou autre chose. Si quelque chose est lent, vous devez toujours être sûr pourquoi avant de savoir ce qui doit changer.
  2. vos besoins sont considérés comme acquis, mais trouver des moyens de les affaiblir est souvent le moyen le plus simple de faire quelque chose plus rapidement.

    • avez-vous vraiment besoin d'une insertion et d'une suppression à temps constant partout, ou seulement à l'avant, à l'arrière, ou les deux, mais pas au milieu?
    • avez-vous vraiment besoin de ces contraintes d'invalidation d'itérateur, ou peuvent-elles être assouplies?
    • existe-t-il des modèles d'accès que vous pouvez exploiter? Si vous supprimez fréquemment un élément de la face avant et que vous le remplacez par un nouveau, pouvez-vous simplement le mettre à jour sur place?
87
Useless

Comme alternative, vous pouvez utiliser un tableau évolutif et gérer les liens explicitement, en tant qu'index dans le tableau.

Les éléments de tableau inutilisés sont placés dans une liste chaînée à l'aide de l'un des liens. Lorsqu'un élément est supprimé, il est renvoyé à la liste libre. Lorsque la liste gratuite est épuisée, agrandissez le tableau et utilisez l'élément suivant.

Pour les nouveaux éléments gratuits, vous avez deux options:

  • les ajouter à la liste gratuite à la fois,
  • ajoutez-les à la demande, en fonction du nombre d'éléments de la liste gratuite par rapport à la taille du tableau.
18
Yves Daoust

L'obligation de ne pas invalider les itérateurs, sauf celui sur un nœud supprimé, interdit à chaque conteneur qui n'alloue pas de nœuds individuels et est très différent de, par exemple. list ou map.
Cependant, j'ai trouvé que dans presque tous les cas quand je pensais que c'était nécessaire, cela s'est avéré avec un peu de discipline dont je pouvais tout aussi bien me passer. Vous voudrez peut-être vérifier si vous le pouvez, vous en bénéficieriez grandement.

Alors que std::list Est en effet la chose "correcte" si vous avez besoin de quelque chose comme une liste (pour la classe CS, principalement), l'affirmation selon laquelle c'est presque toujours le mauvais choix est, malheureusement, exactement la bonne. Bien que la revendication O(1) soit entièrement vraie, elle est néanmoins abyssale par rapport au fonctionnement du matériel informatique réel, ce qui lui donne un énorme facteur constant. Notez que non seulement les objets que vous itérez placés au hasard, mais les nœuds que vous maintenez le sont aussi (oui, vous pouvez en quelque sorte contourner cela avec un allocateur, mais ce n'est pas le point). En moyenne, vous avez deux un cache garanti manque pour tout ce que vous faites, plus jusqu'à deux une allocation dynamique pour les opérations de mutation (une pour l'objet et une autre pour le nœud).

Edit: Comme indiqué par @ratchetfreak ci-dessous, les implémentations de std::list Réduisent généralement l'allocation d'objets et de nœuds dans un bloc de mémoire comme une optimisation (semblable à ce que par exemple make_shared le fait), ce qui rend le cas moyen un peu moins catastrophique ( une allocation par mutation et un échec de cache garanti au lieu de deux).
Une considération nouvelle et différente dans ce cas pourrait être que cela ne soit pas entièrement sans problème non plus. Postfixer l'objet avec deux pointeurs signifie inverser la direction tout en déréférencé, ce qui peut interférer avec la pré-lecture automatique.
Le préfixe de l'objet avec les pointeurs, d'autre part, signifie que vous repoussez l'objet de la taille de deux pointeurs, ce qui signifie jusqu'à 16 octets sur un système 64 bits (qui pourrait diviser un un objet de taille supérieure aux limites des lignes de cache à chaque fois). De plus, il faut considérer que std::list Ne peut pas se permettre de casser par exemple SSE code uniquement parce qu'il ajoute un décalage clandestin comme surprise spéciale (ainsi, par exemple, le xor-trick ne serait probablement pas applicable pour réduire l'empreinte à deux points). Il devrait probablement y en avoir quantité de rembourrage "sûr" pour vous assurer que les objets ajoutés à une liste fonctionnent toujours comme ils le devraient.
Je ne suis pas en mesure de dire s'il s'agit de problèmes de performances réels ou simplement de méfiance et de peur de mon côté, mais je pense qu'il est juste de dire qu'il peut y avoir plus de serpents cachés dans l'herbe qu'on ne le pense.

Ce n'est pas pour rien que des experts C++ de haut niveau (Stroustrup, notamment) recommandent d'utiliser std::vector, Sauf si vous avez une très bonne raison de ne pas le faire.

Comme beaucoup de gens auparavant, j'ai essayé d'être intelligent pour utiliser (ou inventer) quelque chose de mieux que std::vector Pour l'un ou l'autre problème particulier et particulier où il semble que vous puissiez faire mieux, mais il s'avère que tout simplement utiliser std::vector est toujours presque toujours la meilleure ou la deuxième meilleure option (si std::vector n'est pas la meilleure, std::deque est généralement ce dont vous avez besoin à la place).
Vous avez beaucoup moins d'allocations qu'avec toute autre approche, beaucoup moins de fragmentation de la mémoire, beaucoup moins d'indirections et un modèle d'accès à la mémoire beaucoup plus favorable. Et devinez quoi, il est facilement disponible et fonctionne tout simplement.
Le fait que de temps en temps les encarts nécessitent une copie de tous les éléments est (généralement) un problème absolu. Vous pensez que c'est le cas, mais ce n'est pas le cas. Cela se produit rarement et c'est une copie d'un bloc de mémoire linéaire, ce qui est exactement ce que les processeurs sont bons (par opposition à de nombreuses doubles indirections et sauts aléatoires sur la mémoire).

Si l'exigence de ne pas invalider les itérateurs est vraiment un must absolu, vous pouvez par exemple coupler un std::vector D'objets avec un ensemble de bits dynamique ou, à défaut de mieux, un std::vector<bool>. Utilisez ensuite reserve() de manière appropriée pour éviter les réallocations. Lors de la suppression d'un élément, ne le supprimez pas mais marquez-le uniquement comme supprimé dans le bitmap (appelez le destructeur à la main). Aux moments appropriés, lorsque vous savez que vous pouvez invalider les itérateurs, appelez une fonction "aspirateur" qui compacte à la fois le vecteur bit et le vecteur objet. Là, toutes les invalidations imprévisibles de l'itérateur ont disparu.

Oui, cela nécessite de conserver un bit supplémentaire "L'élément a été supprimé", ce qui est gênant. Mais un std::list Doit aussi maintenir deux pointeurs, en plus de l'objet réel, et il doit faire des allocations. Avec le vecteur (ou deux vecteurs), l'accès est toujours très efficace, car il se fait de manière compatible avec le cache. L'itération, même lors de la recherche de nœuds supprimés, signifie toujours que vous vous déplacez linéairement ou presque linéairement sur la mémoire.

18
Damon

std::list est une liste doublement chaînée, donc malgré son inefficacité dans la construction des éléments, il supporte insérer/supprimer dans O(1) complexité temporelle, mais cette fonctionnalité est complètement ignoré dans ce paragraphe cité.

C'est ignoré parce que c'est un mensonge.

Le problème de la complexité algorithmique est qu'elle mesure généralement une chose . Par exemple, lorsque nous disons que l'insertion dans un std::map est O (log N), nous voulons dire qu'il effectue des comparaisons O (log N) . Les coûts de itération , extraction des lignes de cache de la mémoire , etc. ne sont pas pris en compte.

Cela simplifie considérablement l'analyse, bien sûr, mais malheureusement, ne correspond pas nécessairement de manière claire aux complexités de mise en œuvre du monde réel. En particulier, une hypothèse flagrante est que l'allocation de mémoire est à temps constant . Et cela, c'est un mensonge audacieux.

Les allocateurs de mémoire à usage général (malloc et co) n'ont aucune garantie sur la complexité la plus défavorable des allocations de mémoire. Le pire des cas dépend généralement du système d'exploitation, et dans le cas de Linux, il peut impliquer le tueur OOM (passer au crible les processus en cours et en tuer un pour récupérer sa mémoire).

Les allocateurs de mémoire à usage spécial pourraient potentiellement être rendus à temps constant ... dans une plage particulière de nombre d'allocations (ou taille d'allocation maximale). Étant donné que la notation Big-O concerne la limite à l'infini, elle ne peut pas être appelée O (1).

Et donc, là où le caoutchouc rencontre la route , la mise en œuvre de std::list ne présente PAS de fonctionnalité générale O(1) insertion/suppression, car l'implémentation repose sur un véritable allocateur de mémoire, pas idéal.


C'est assez déprimant, mais vous ne devez pas perdre tous vos espoirs.

Plus particulièrement, si vous pouvez déterminer une limite supérieure au nombre d'éléments et pouvez allouer autant de mémoire à l'avance, alors vous pouvez créer un allocateur de mémoire qui effectuera l'allocation de mémoire à temps constant, vous donnant l'illusion de O (1).

16
Matthieu M.

Utilisez deux std::lists: une "liste libre" qui est préallouée avec une grande réserve de nœuds au démarrage, et l'autre liste "active" dans laquelle vous splice nœuds de la liste libre. C'est un temps constant et ne nécessite pas d'allouer un nœud.

7
Mark B

La nouvelle slot_map revendication de proposition O(1) pour l'insertion et la suppression.

Il existe également un lien vers un vidéo avec une mise en œuvre proposée et des travaux antérieurs.

Si nous en savions plus sur la structure réelle des éléments, il pourrait y avoir des conteneurs associatifs spécialisés qui sont bien meilleurs.

5
Surt

Je suggérerais de faire exactement ce que dit @Yves Daoust, sauf qu'au lieu d'utiliser une liste chaînée pour la liste gratuite, utilisez un vecteur. Poussez et éclatez les indices libres à l'arrière du vecteur. Ceci est amorti O(1) insérer, rechercher et supprimer, et n'implique pas de poursuite de pointeur. Il ne nécessite également aucune activité d'allocateur ennuyeuse.

4
Dan

Je voulais juste faire un petit commentaire sur votre choix. Je suis un grand fan de vecteur en raison de ses vitesses de lecture, et vous pouvez accéder directement à n'importe quel élément et faire le tri si nécessaire. (vecteur de classe/struct par exemple).

Mais de toute façon je m'égare, il y a deux astuces astucieuses que je voulais divulguer. Avec les insertions vectorielles, cela peut coûter cher, donc une astuce intéressante, n'insérez pas si vous pouvez vous en sortir sans le faire. faites un Push_back normal (mis à la fin) puis échangez l'élément avec celui que vous voulez.

Même chose avec les suppressions. Ils sont chers. Échangez-le donc avec le dernier élément, supprimez-le.

2
ViperG

J'appuie la réponse de @Useless, en particulier le point 2 de PS sur la révision des exigences. Si vous relâchez la contrainte d'invalidation de l'itérateur, utilisez std::vector<> est suggestion standard de Stroustrup pour un conteneur à petit nombre d'articles (pour les raisons déjà mentionnées dans les commentaires). Connexesquestions sur SO.

À partir de C++ 11, il y a aussi std::forward_list.

De plus, si l'allocation de tas standard pour les éléments ajoutés au conteneur n'est pas suffisante, je dirais que vous devez regarder attentivement votre exigences exactes et affinez-les.

2
Pablo H

Merci pour toutes les réponses. Il s'agit d'une référence simple - mais pas rigoureuse -.

// list.cc
#include <list>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        list<size_t> ln;
        for (size_t i = 0; i < 200; i++) {
            ln.insert(ln.begin(), i);
            if (i != 0 && i % 20 == 0) {
                ln.erase(++++++++++ln.begin());
            }
        }
    }
}

et

// vector.cc
#include <vector>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        vector<size_t> vn;
        for (size_t i = 0; i < 200; i++) {
            vn.insert(vn.begin(), i);
            if (i != 0 && i % 20 == 0) {
                vn.erase(++++++++++vn.begin());
            }
        }
    }
}

Ce test vise à tester ce que std::list Prétend à Excel à - [~ # ~] o [~ # ~] (1) en insérant et en effaçant. Et, en raison des positions que je demande d'insérer/supprimer, cette course est fortement biaisée par rapport à std::vector, Car elle doit déplacer tous les éléments suivants (d'où [~ # ~] o [~ # ~] (n)), alors que std::list n'a pas besoin de le faire.

Maintenant je les compile.

clang++ list.cc -o list
clang++ vector.cc -o vector

Et testez le runtime. Le résultat est:

  time ./list
  ./list  4.01s user 0.05s system 91% cpu 4.455 total
  time ./vector
  ./vector  1.93s user 0.04s system 78% cpu 2.506 total

std::vector A gagné.

Compilé avec l'optimisation O3, std::vector Gagne toujours.

  time ./list
  ./list  2.36s user 0.01s system 91% cpu 2.598 total
  time ./vector
  ./vector  0.58s user 0.00s system 50% cpu 1.168 total

std::list Doit appeler l'allocation de tas pour chaque élément , tandis que std::vector Peut allouer de la mémoire de tas en batch (bien que cela puisse dépend de l'implémentation), donc l'insertion/suppression de std::list a un facteur constant plus élevé, bien qu'il soit [~ # ~] o [~ # ~] (1) .

Pas étonnant ce document dit

std::vector Est bien aimé et respecté.

[~ # ~] modifier [~ # ~]: std::deque fait encore mieux dans certains cas, au moins pour cette tâche .

// deque.cc
#include <deque>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        deque<size_t> dn;
        for (size_t i = 0; i < 200; i++) {
            dn.insert(dn.begin(), i);
            if (i != 0 && i % 20 == 0) {
                dn.erase(++++++++++dn.begin());
            }
        }
    }
}

Sans optimisation:

./deque  2.13s user 0.01s system 86% cpu 2.470 total

Optimisé avec O3:

./deque  0.27s user 0.00s system 50% cpu 0.551 total
1
Leedehai