Une récente discussion sur unordered_map
en C++ m'a fait comprendre que je devrais utiliser unordered_map
dans la plupart des cas où j’avais utilisé map
auparavant, en raison de l’efficacité de la recherche (amortised O(1) vs O (log n)). La plupart du temps, j'utilise une mappe que j'utilise soit la int
's, soit le std::strings
comme clés; par conséquent, la définition de la fonction de hachage ne me pose aucun problème. Plus j'y pensais, plus je réalisais que je ne trouvais aucune raison d'utiliser un std::map
en cas de types simples sur un unordered_map
- j'ai jeté un coup d'œil aux interfaces et je n'ai trouvé aucune signification significative. différences qui auraient un impact sur mon code.
D'où la question suivante: existe-t-il une raison réelle d'utiliser std::map
sur unordered map
dans le cas de types simples tels que int
et std::string
?
Je pose la question du point de vue strictement de la programmation - je sais que ce n’est pas vraiment considéré comme une norme et que cela peut poser des problèmes de portage.
J'espère aussi que l'une des bonnes réponses pourrait être "c'est plus efficace pour les plus petits ensembles de données" en raison d'une surcharge moins importante (est-ce vrai?) - par conséquent, j'aimerais limiter la question aux cas où le nombre de clés est non négligeable (> 1 024).
Edit: duh, j'ai oublié l'évident (merci GMan!) - oui, les cartes sont commandées bien sûr - je le sais et je cherche d'autres raisons.
N'oubliez pas que les map
conservent leurs éléments en ordre. Si vous ne pouvez pas abandonner cela, vous ne pouvez évidemment pas utiliser un unordered_map
.
Il faut aussi garder à l'esprit que unordered_map
utilise généralement plus de mémoire. Un map
a juste quelques pointeurs de conservation, puis de la mémoire pour chaque objet. Au contraire, les unordered_map
ont un grand tableau (ceux-ci peuvent être très gros dans certaines implémentations) et ensuite de la mémoire supplémentaire pour chaque objet. Si vous avez besoin de connaître la mémoire, un map
devrait s'avérer meilleur, car il manque le grand tableau.
Donc, si vous avez besoin d'une recherche pure, ce serait plutôt un unordered_map
. Mais il y a toujours des compromis, et si vous ne pouvez pas vous les payer, vous ne pouvez pas les utiliser.
Juste par expérience personnelle, j’ai constaté une énorme amélioration des performances (mesurée bien entendu) lorsqu’on a utilisé un unordered_map
au lieu d’un map
dans une table de recherche d’entités principales.
D'autre part, j'ai constaté que l'insertion et le retrait répétés d'éléments étaient beaucoup plus lents. C'est génial pour une collection d'éléments relativement statique, mais si vous faites des tonnes d'insertions et de suppressions, le hachage + le seau semble s'additionner. (Remarque, cela a pris plusieurs itérations.)
Si vous souhaitez comparer la vitesse de vos implémentations std::map
et std::unordered_map
, vous pouvez utiliser le projet sparsehash de Google qui dispose d'un programme time_hash_map. Par exemple, avec gcc 4.4.2 sur un système Linux x86_64
$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow 126.1 ns (27427396 hashes, 40000000 copies) 290.9 MB
map_predict/grow 67.4 ns (10000000 hashes, 40000000 copies) 232.8 MB
map_replace 22.3 ns (37427396 hashes, 40000000 copies)
map_fetch 16.3 ns (37427396 hashes, 40000000 copies)
map_fetch_empty 9.8 ns (10000000 hashes, 0 copies)
map_remove 49.1 ns (37427396 hashes, 40000000 copies)
map_toggle 86.1 ns (20000000 hashes, 40000000 copies)
STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow 225.3 ns ( 0 hashes, 20000000 copies) 462.4 MB
map_predict/grow 225.1 ns ( 0 hashes, 20000000 copies) 462.6 MB
map_replace 151.2 ns ( 0 hashes, 20000000 copies)
map_fetch 156.0 ns ( 0 hashes, 20000000 copies)
map_fetch_empty 1.4 ns ( 0 hashes, 0 copies)
map_remove 141.0 ns ( 0 hashes, 20000000 copies)
map_toggle 67.3 ns ( 0 hashes, 20000000 copies)
Je ferais écho à peu près au même argument de GMan: selon le type d’utilisation, std::map
peut être (et est souvent) plus rapide que std::tr1::unordered_map
(à l’aide de l’implémentation incluse dans VS 2008 SP1).
Il y a quelques facteurs de complication à garder à l'esprit. Par exemple, dans std::map
, vous comparez des clés, ce qui signifie que vous ne regardez jamais suffisamment le début d'une clé pour distinguer les sous-branches droite et gauche de l'arborescence. D'après mon expérience, presque toute la clé que vous examinez est d'utiliser un élément tel que int que vous pouvez comparer en une seule instruction. Avec un type de clé plus typique comme std :: string, vous ne comparez souvent que quelques caractères.
En revanche, une fonction de hachage décente regarde toujours la touche entière . IOW, même si la recherche dans la table est de complexité constante, le hachage lui-même présente une complexité approximativement linéaire (bien que ce soit sur la longueur de la clé, pas sur le nombre d'éléments). Avec de longues chaînes comme clés, un std::map
peut terminer une recherche avant un unordered_map
même démarrer sa recherche.
Deuxièmement, bien qu'il existe plusieurs méthodes de redimensionnement des tables de hachage, la plupart d'entre elles sont assez lentes - au point que, sauf si les recherches sont considérablement plus fréquentes que les insertions et les suppressions, std :: map sera souvent plus rapide que std::unordered_map
.
Bien sûr, comme je l'ai mentionné dans le commentaire de votre question précédente, vous pouvez également utiliser une table d'arbres. Cela présente des avantages et des inconvénients. D'une part, il limite le pire des cas à celui d'un arbre. Cela permet également une insertion et une suppression rapides, car (du moins lorsque je l'ai fait), j'ai utilisé une table de taille fixe. L'élimination du redimensionnementall table vous permet de garder votre table de hachage beaucoup plus simple et généralement plus rapide.
Un autre point: les exigences pour le hachage et les cartes basées sur des arbres sont différentes. Le hachage nécessite évidemment une fonction de hachage et une comparaison d'égalité, où les cartes ordonnées nécessitent une comparaison moins que. Bien sûr, l'hybride que j'ai mentionné nécessite les deux. Bien sûr, dans le cas courant où une chaîne est utilisée comme clé, ce n'est pas vraiment un problème, mais certains types de clés conviennent mieux à la commande qu'au hachage (ou inversement).
La réponse de @Jerry Coffin, qui suggérait que la carte ordonnée affichait des augmentations de performances sur de longues chaînes, m'a intriguée, après quelques expériences (qui peut être téléchargée à partir de Pastebin ), j'ai constaté que cela ne semble C’est vrai pour les collections de chaînes aléatoires, lorsque la carte est initialisée avec un dictionnaire trié (qui contient des mots avec des quantités considérables de préfixe-chevauchement), cette règle s’efface, probablement à cause de la profondeur accrue de l’arbre nécessaire pour récupérer la valeur. Les résultats sont indiqués ci-dessous, la première colonne du nombre correspond au temps d'insertion, la deuxième à l'heure de récupération.
g++ -g -O3 --std=c++0x -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
** Integer Keys **
unordered: 137 15
ordered: 168 81
** Random String Keys **
unordered: 55 50
ordered: 33 31
** Real Words Keys **
unordered: 278 76
ordered: 516 298
Je voudrais juste souligner que ... il y a beaucoup de types de unordered_map
s.
Recherchez le Article Wikipedia sur la carte de hachage. Selon l’implémentation utilisée, les caractéristiques de recherche, d’insertion et de suppression peuvent varier considérablement.
Et c’est ce qui m’inquiète le plus avec l’ajout de unordered_map
au STL: ils devront choisir une implémentation particulière, car je doute qu’ils empruntent la route Policy
, et nous serons donc coincés avec une implémentation pour un usage moyen et rien pour les autres cas ...
Par exemple, certaines cartes de hachage ont un rehachage linéaire, où au lieu de rehase la carte de hachage entière en une fois, une partie est réhachée à chaque insertion, ce qui permet d’amortir le coût.
Autre exemple: certaines cartes de hachage utilisent une simple liste de noeuds pour un compartiment, d'autres utilisent une carte, d'autres n'utilisent pas de noeuds mais recherchent l'emplacement le plus proche et enfin certains utilisent une liste de noeuds mais la réorganisent de manière à ce que le dernier élément consulté est à l'avant (comme une chose en cache).
Donc, pour le moment, j'ai tendance à préférer le std::map
ou peut-être un loki::AssocVector
(pour les ensembles de données gelés).
Ne vous méprenez pas, j'aimerais utiliser le std::unordered_map
et je le ferai peut-être à l'avenir, mais il est difficile de "faire confiance" à la portabilité d'un tel conteneur lorsque vous réfléchissez à toutes les manières de le mettre en œuvre et aux différentes performances associées. résultat de cela.
Les tables de hachage ont des constantes plus élevées que les implémentations de cartes communes, qui deviennent significatives pour les petits conteneurs. La taille maximale est de 10, 100 ou peut-être même 1 000 ou plus? Les constantes sont les mêmes, mais O (log n) est proche de O (k). (Rappelez-vous la complexité logarithmique est encore vraiment bon.)
Ce qui fait une bonne fonction de hachage dépend des caractéristiques de vos données. donc, si je n'ai pas l'intention de regarder une fonction de hachage personnalisée (mais que je peux certainement changer d'avis plus tard, et facilement puisque je tape à peu près partout) et même si les valeurs par défaut sont choisies pour fonctionner correctement pour de nombreuses sources de données, nature de la carte pour être une aide suffisante au départ que je mette toujours par défaut à mapper plutôt qu’une table de hachage dans ce cas.
De plus, vous n'avez même pas à penser à écrire une fonction de hachage pour d'autres types (généralement du type UDT), et vous n'avez qu'à écrire op <(ce que vous voulez quand même).
map
maintient les itérateurs de tous les éléments stables. En C++ 17, vous pouvez même déplacer des éléments d'un map
à l'autre sans invalider les itérateurs invalidants (et s'ils sont correctement implémentés sans aucune allocation potentielle).map
pour des opérations uniques sont généralement plus cohérents, car ils n'ont jamais besoin d'allocations importantes.unordered_map
utiliser std::hash
tel qu'implémenté dans libstdc ++ est vulnérable au DoS s'il est alimenté avec une entrée non fiable (il utilise MurmurHash2 avec une valeur de départ constante - si l'ensemencement aiderait réellement, voir https://emboss.github.io/blog/2012/ 12/14/coupure-murmure-hachage-inondation-dos-rechargé/ ).J'ai récemment effectué un test qui permet de faire 50000 fusion et tri. Cela signifie que si les clés de chaîne sont identiques, fusionnez la chaîne d'octet. Et le résultat final devrait être trié. Donc, cela inclut une recherche pour chaque insertion.
Pour l'implémentation map
, il faut 200 ms pour terminer le travail. Pour unordered_map
+ map
, il faut 70 ms pour l'insertion unordered_map
et 80 ms pour l'insertion map
. L'implémentation hybride est donc 50 ms plus rapide.
Nous devrions réfléchir à deux fois avant d’utiliser map
. Si vous souhaitez uniquement que les données soient triées dans le résultat final de votre programme, une solution hybride peut être préférable.
Les raisons ont été données dans d'autres réponses; en voici un autre.
les opérations std :: map (arbre binaire équilibré) sont amorties O (log n) et les cas les plus défavorables O (log n) . Les opérations std :: unordered_map (table de hachage) sont amorties O(1) et les plus mauvaises cas O (n).
Dans la pratique, cela se traduit par le fait que la table de hachage "fait des ratés" de temps en temps avec une opération O(n), ce que votre application peut tolérer ou non. S'il ne peut pas le tolérer, vous préféreriez std :: map à std :: unordered_map.
Petit ajout à tout ce qui précède:
Mieux vaut utiliser map
lorsque vous devez obtenir des éléments par plage, car ils sont triés et vous pouvez simplement les parcourir d'une limite à l'autre.