web-dev-qa-db-fra.com

Comment std :: unordered_map est implémenté

traitement des collisions c ++ unordered_map, redimensionnement et redimensionnement

C'est une question que j'ai déjà posée et j'ai constaté que je suis très confus quant à la manière dont unordered_map est implémenté. Je suis sûr que beaucoup d'autres personnes partagent cette confusion avec moi. Sur la base des informations que je connais sans lire la norme:

Chaque implémentation unordered_map stocke une liste liée à des nœuds externes dans le tableau de compartiments ... Non, ce n'est pas du tout le moyen le plus efficace d'implémenter une carte de hachage pour les utilisations les plus courantes. Malheureusement, un petit "oubli" dans la spécification de unordered_map requiert tout simplement ce comportement. Le comportement requis est que les itérateurs aux éléments doivent rester valides lors de l'insertion ou de la suppression d'autres éléments.

J'espérais que quelqu'un pourrait expliquer l'implémentation et son adaptation à la définition standard C++ (en termes d'exigences de performances) et si ce n'est vraiment pas le moyen le plus efficace d'implémenter une structure de données de carte de hachage, comment l'améliorer?

46
ralzaul

La norme impose efficacement les implémentations std::unordered_set Et std::unordered_map Qui utilisent un hachage à ciel ouvert, ce qui signifie un tableau de compartiments, chacun contenant la tête d'une liste logique (et généralement réelle). Cette exigence est subtile: le facteur de charge maximal par défaut étant de 1,0 et la garantie que le tableau ne sera pas remanié à moins de dépasser ce facteur de charge: cela serait impossible sans enchaînement, car les collisions avec hachage fermé deviennent écrasantes facteur de charge proche de 1:

23.2.5/15: Les membres insert et emplace n’affecteront pas la validité des itérateurs si (N+n) < z * B, Où N est le nombre d’éléments du conteneur avant l'opération d'insertion, n est le nombre d'éléments insérés, B est le nombre de compartiments du conteneur et z est le facteur de charge maximum du conteneur.

parmi les effets du constructeur à 23.5.4.2/1: max_load_factor() renvoie 1.0.

(Pour permettre une itération optimale sans passer par des compartiments vides, l'implémentation de GCC les remplit avec des itérateurs dans une seule liste à lien unique contenant toutes les valeurs: les itérateurs pointent sur l'élément immédiatement avant les éléments de ce compartiment, afin que le pointeur suivant puisse être Rewired si effacer la dernière valeur du compartiment.)

En ce qui concerne le texte que vous citez:

Non, ce n'est pas du tout le moyen le plus efficace d'implémenter une carte de hachage pour les utilisations les plus courantes. Malheureusement, un petit "oubli" dans la spécification de unordered_map requiert tout simplement ce comportement. Le comportement requis est que les itérateurs aux éléments doivent rester valides lors de l'insertion ou de la suppression d'autres éléments.

Il n'y a pas de "surveillance" ... ce qui a été fait était très délibéré et réalisé en pleine connaissance de cause. Il est vrai que d’autres compromis auraient pu être trouvés, mais l’approche ouverte de hachage/chaînage est un compromis raisonnable pour un usage général, qui gère raisonnablement les collisions avec des fonctions de hachage médiocres, ne gaspille pas trop avec des types de clé/valeur de taille petite ou grande, et gère arbitrairement un grand nombre de insert/erase paires sans dégrader progressivement les performances, comme le font de nombreuses implémentations de hachage fermé.

Comme preuve de la prise de conscience, de ici la proposition de Matthew Austern :

Je ne suis au courant d'aucune implémentation satisfaisante d'adressage ouvert dans un cadre générique. L’adressage ouvert pose un certain nombre de problèmes:

• Il est nécessaire de faire la distinction entre un poste vacant et un poste occupé.

• Il est nécessaire de limiter la table de hachage aux types dotés d'un constructeur par défaut et de construire chaque élément de tableau à l'avance, ou de conserver un tableau dont certains éléments sont des objets et d'autres, de la mémoire brute.

• L'adressage ouvert rend la gestion des collisions difficile: si vous insérez un élément dont le code de hachage correspond à un emplacement déjà occupé, vous avez besoin d'une stratégie qui vous indique où essayer ensuite. C'est un problème résolu, mais les solutions les plus connues sont compliquées.

• La gestion des collisions est particulièrement compliquée lorsque l'effacement d'éléments est autorisé. (Voir Knuth pour une discussion.) Une classe conteneur pour la bibliothèque standard devrait permettre l'effacement.

• Les systèmes de gestion des collisions pour l'adressage ouvert ont tendance à prendre pour hypothèse un tableau de taille fixe pouvant contenir jusqu'à N éléments. Une classe de conteneur pour la bibliothèque standard doit pouvoir se développer si nécessaire lorsque de nouveaux éléments sont insérés, dans la limite de la mémoire disponible.

La résolution de ces problèmes pourrait constituer un projet de recherche intéressant, mais, en l'absence d'expérience en implémentation dans le contexte de C++, il serait inapproprié de normaliser une classe de conteneur à adressage ouvert.

Spécifiquement pour les tables contenant uniquement des insertions avec des données suffisamment petites pour être stockées directement dans les compartiments, une valeur de sentinelle pratique pour les compartiments inutilisés et une fonction de hachage efficace, une approche de hachage fermé peut être plus ou moins rapide et utiliser beaucoup moins de mémoire, mais ce n'est pas un but général.

Une comparaison complète et l’élaboration des options de conception de table de hachage et de leurs implications sont hors sujet pour S.O. comme il est beaucoup trop large pour aborder correctement ici.

60
Tony Delroy