Y a-t-il un avantage à utiliser map sur unordered_map en cas de clés triviales?

371

Une récente discussion unordered_mapen C ++ m'a fait réaliser que je devrais utiliser unordered_mapdans la plupart des cas où je l'ai utilisé mapauparavant, en raison de l'efficacité de la recherche ( O (1) amorti vs O (log n) ). La plupart du temps, j'utilise une carte, j'utilise soit intou std::stringcomme type de clé; par conséquent, je n'ai aucun problème avec la définition de la fonction de hachage. Plus j'y réfléchissais, plus je me rendais compte que je ne trouvais aucune raison d'utiliser un std::mapsur un std::unordered_mapdans le cas de clés avec des types simples - j'ai jeté un coup d'œil aux interfaces, et je n'ai trouvé aucun des différences importantes qui auraient un impact sur mon code.

D' où la question: est - il une vraie raison d'utiliser std::mapplus std::unordered_mapdans le cas des types simples comme intet std::string?

Je demande d'un point de vue strictement programmatique - je sais que ce n'est pas entièrement considéré comme standard et que cela peut poser des problèmes de portage.

De plus, je m'attends à ce que l'une des bonnes réponses soit «c'est plus efficace pour des ensembles de données plus petits» en raison d'un surcoût plus petit (est-ce vrai?) - je voudrais donc limiter la question aux cas où la quantité de clés n'est pas trivial (> 1 024).

Edit: duh, j'ai oublié l'évidence (merci GMan!) - oui, les cartes sont commandées bien sûr - je le sais, et je cherche d'autres raisons.

Kornel Kisielewicz
la source
22
J'aime poser cette question dans les interviews: "Quand est-ce que le tri rapide est meilleur que le tri à bulles?" La réponse à la question donne un aperçu de l'application pratique de la théorie de la complexité et pas seulement des énoncés simples en noir et blanc tels que O (1) est meilleur que O (n) ou O (k) est équivalent à O (logn) etc. ..
42
@Beh, je pense que vous vouliez dire "quand le tri à bulles est-il meilleur que le tri rapide": P
Kornel Kisielewicz
2
Un pointeur intelligent serait-il une clé triviale?
thomthom
Voici l'un des cas où la carte est la plus avantageuse: stackoverflow.com/questions/51964419/…
anilbey

Réponses:

399

N'oubliez pas que cela mapgarde ses éléments ordonnés. Si vous ne pouvez pas abandonner cela, vous ne pouvez évidemment pas l'utiliser unordered_map.

Une autre chose à garder à l'esprit est qu'elle unordered_maputilise généralement plus de mémoire. mapa juste quelques pointeurs de ménage et de la mémoire pour chaque objet. A l'inverse, unordered_mapa un grand tableau (ceux-ci peuvent devenir assez gros dans certaines implémentations), puis de la mémoire supplémentaire pour chaque objet. Si vous devez être conscient de la mémoire, cela mapdevrait s'avérer mieux, car il manque le grand tableau.

Donc, si vous avez besoin d'une recherche-récupération pure, je dirais que unordered_mapc'est la voie à suivre. Mais il y a toujours des compromis, et si vous ne pouvez pas vous les permettre, vous ne pouvez pas l'utiliser.

Juste par expérience personnelle, j'ai trouvé une énorme amélioration des performances (mesurée, bien sûr) lors de l'utilisation unordered_mapau lieu d' mapune table de recherche d'entité principale.

D'un autre côté, j'ai trouvé qu'il était beaucoup plus lent d'insérer et de retirer des éléments à plusieurs reprises. C'est génial pour une collection d'éléments relativement statiques, mais si vous faites des tonnes d'insertions et de suppressions, le hachage + le regroupement semble s'additionner. (Remarque, cela a duré de nombreuses itérations.)

GManNickG
la source
3
Une dernière chose à propos de la grande propriété de bloc de mémoire (r) de unordered_map vs map (ou vector vs list), le tas de processus par défaut (en parlant de Windows ici) est sérialisé. L'allocation de (petits) blocs en grandes quantités dans une application multithread est très coûteuse.
ROAR
4
RA: Vous pouvez contrôler cela avec votre propre type d'allocateur combiné avec n'importe quel conteneur, si vous pensez que cela importe pour un programme particulier.
9
Si vous connaissez la taille unordered_mapet réservez au début - payez-vous toujours une pénalité de nombreuses insertions? Supposons que vous n'insérez qu'une seule fois lorsque vous avez créé la table de recherche, puis que vous n'y lisez plus tard.
thomthom
3
@thomthom Pour autant que je sache, il ne devrait pas y avoir de pénalité en termes de performances. La raison pour laquelle les performances prennent un coup est due au fait que si le tableau devient trop volumineux, il fera une refonte de tous les éléments. Si vous appelez reserve, cela remaniera potentiellement les éléments existants mais si vous l'appelez au début, il ne devrait pas y avoir de pénalité, du moins selon cplusplus.com/reference/unordered_map/unordered_map/reserve
Richard Fung
6
Je suis tout à fait sûr qu'en ce qui concerne la mémoire, c'est le contraire. En supposant le facteur de charge 1.0 par défaut pour un conteneur non ordonné: vous avez un pointeur par élément pour le compartiment et un pointeur par élément pour l'élément suivant dans le compartiment, vous vous retrouvez donc avec deux pointeurs plus des données pour chaque élément. Pour un conteneur ordonné, en revanche, une implémentation d'arbre RB typique aura: trois pointeurs (gauche / droite / parent) plus un bit de couleur qui, en raison de l'alignement, prend un quatrième mot. C'est quatre pointeurs plus des données pour chaque élément.
Yakov Galka
126

Si vous voulez comparer la vitesse de vos std::mapet std::unordered_mapmises en œuvre, vous pouvez utiliser Google sparsehash projet qui a un programme de time_hash_map en temps eux. 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)
Blair Zajac
la source
2
Il semble que la carte non ordonnée bat la carte sur la plupart des opérations. Événement lors de l'insertion ...
Michael IV
7
sparsehash n'existe plus. il a été supprimé ou retiré.
User9102d82
1
@ User9102d82 J'ai édité la question pour faire référence à un lien waybackmachine .
andreee
Juste pour s'assurer que les autres remarquent également les autres nombres en plus du temps: Ces tests ont été effectués avec des objets / infrastructures de 4 octets, alias un int. Si vous stockez quelque chose qui nécessite un hachage plus lourd ou qui est plus grand (ce qui alourdit les opérations de copie), la carte standard pourrait rapidement avoir un avantage!
AlexGeorg
82

Je ferais à peu près écho au même point que GMan a fait valoir: selon le type d'utilisation, il std::mappeut être (et est souvent) plus rapide que std::tr1::unordered_map(en utilisant l'implémentation incluse dans VS 2008 SP1).

Il y a quelques facteurs compliquant à garder à l'esprit. Par exemple, dans std::map, vous comparez des clés, ce qui signifie que vous ne regardez que suffisamment le début d'une clé pour faire la distinction entre les sous-branches droite et gauche de l'arborescence. D'après mon expérience, la seule fois où vous regardez une clé entière est si vous utilisez quelque chose comme 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.

Une fonction de hachage décente, en revanche, examine toujours la clé entière . IOW, même si la recherche de table est à complexité constante, le hachage lui-même a une complexité à peu près linéaire (bien que sur la longueur de la clé, pas sur le nombre d'éléments). Avec de longues chaînes comme clés, un std::mappourrait terminer une recherche avant unordered_mapmême de commencer 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 qu'à moins que les recherches ne soient 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 un tableau d'arbres. Cela présente à la fois des avantages et des inconvénients. D'une part, il limite le pire des cas à celui d'un arbre. Il permet également une insertion et une suppression rapides, car (au moins quand je l'ai fait), j'ai utilisé une table de taille fixe. L'élimination de tout redimensionnement de 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 arborescentes 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 inférieure à. Bien sûr, l'hybride que j'ai mentionné nécessite les deux. Bien sûr, dans le cas commun de l'utilisation d'une chaîne comme clé, ce n'est pas vraiment un problème, mais certains types de clés conviennent mieux à l'ordre que le hachage (ou vice versa).

Jerry Coffin
la source
2
Le redimensionnement du hachage peut être atténué par les dynamic hashingtechniques, qui consistent à avoir une période de transition où chaque fois que vous insérez un élément, vous remaniez également les kautres éléments. Bien sûr, cela signifie que pendant la transition, vous devez rechercher 2 tables différentes ...
Matthieu M.
2
"Avec de longues chaînes comme clés, un std :: map peut terminer une recherche avant qu'un unordered_map ne commence même sa recherche." - si la clé n'est pas présente dans la collection. S'il est présent, alors bien sûr, la longueur totale doit être comparée pour confirmer le match. Mais il unordered_mapfaut également confirmer une correspondance de hachage avec une comparaison complète, donc tout dépend des parties du processus de recherche que vous contrastez.
Steve Jessop
2
vous pouvez généralement remplacer la fonction de hachage en fonction de la connaissance des données. par exemple, si vos longues chaînes varient plus dans les 20 derniers octets que dans les 100 premiers,
hachez
56

J'ai été intrigué par la réponse de @Jerry Coffin, qui a suggéré que la carte ordonnée présenterait des augmentations de performances sur de longues chaînes, après quelques expérimentations (qui peuvent être téléchargées à partir de pastebin ), j'ai trouvé que cela ne semble vrai que 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 chevauchement de préfixes), cette règle tombe en panne, probablement en raison de la profondeur d'arbre accrue nécessaire pour récupérer la valeur. Les résultats sont indiqués ci-dessous, la 1ère colonne de nombre est le temps d'insertion, la 2ème est le temps 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
Gearoid Murphy
la source
2
Merci pour le test. Pour nous assurer que nous ne mesurons pas le bruit, je l'ai changé pour effectuer chaque opération plusieurs fois (et j'ai inséré le compteur au lieu de 1 dans la carte). Je l'ai exécuté sur un nombre différent de clés (de 2 à 1000) et jusqu'à ~ 100 clés sur la carte, std::mapsurpasse généralement std::unordered_map, en particulier pour les clés entières, mais ~ 100 clés, il semble qu'il perd son avantage et std::unordered_mapcommence à gagner. Insérer une séquence déjà ordonnée dans un std::mapest très mauvais, vous obtiendrez son pire scénario (O (N)).
Andreas Magnusson
30

Je voudrais simplement souligner que ... il existe de nombreux types de par unordered_map.

Recherchez l'article Wikipedia sur la carte de hachage. Selon l'implémentation utilisée, les caractéristiques en termes de recherche, d'insertion et de suppression peuvent varier de manière assez significative.

Et c'est ce qui m'inquiète le plus avec l'ajout de unordered_mapla STL: ils devront choisir une implémentation particulière car je doute qu'ils iront sur la Policyroute, et donc nous serons coincés avec une implémentation pour l'utilisation moyenne et rien pour les autres cas ...

Par exemple, certaines cartes de hachage ont un réhachage linéaire, où au lieu de re-hacher toute la carte de hachage à la fois, une partie est re-hachée à chaque insertion, ce qui aide à amortir le coût.

Un autre exemple: certaines cartes de hachage utilisent une simple liste de nœuds pour un compartiment, d'autres utilisent une carte, d'autres n'utilisent pas de nœuds mais trouvent l'emplacement le plus proche et enfin certaines utiliseront une liste de nœuds mais la réorganiseront afin que le dernier élément accédé est à l'avant (comme une chose en cache).

Donc, pour le moment, j'ai tendance à préférer le std::mapou peut-être un loki::AssocVector(pour les ensembles de données figés).

Ne vous méprenez pas, j'aimerais utiliser le std::unordered_mapet je le pourrai à l'avenir, mais il est difficile de "faire confiance" à la portabilité d'un tel conteneur quand on pense à toutes les façons de le mettre en œuvre et aux différentes performances qui en résultent de cela.

Matthieu M.
la source
17
+1: point valide - la vie était plus facile lorsque j'utilisais ma propre implémentation - au moins, je savais elle était nulle:>
Kornel Kisielewicz
25

Différences importantes qui n'ont pas vraiment été mentionnées de manière adéquate ici:

  • mapmaintient les itérateurs de tous les éléments stables, en C ++ 17, vous pouvez même déplacer des éléments de l'un mapà l'autre sans invalider les itérateurs (et s'ils sont correctement implémentés sans allocation potentielle).
  • map les horaires des opérations individuelles sont généralement plus cohérents car ils n'ont jamais besoin d'allocations importantes.
  • unordered_mapl'utilisation std::hashtelle qu'implémentée dans libstdc ++ est vulnérable au DoS si elle est alimentée par des entrées non fiables (elle utilise MurmurHash2 avec une valeur de départ constante - pas que l'amorçage aiderait vraiment, voir https://emboss.github.io/blog/2012/12/14/ rupture-murmure-hachage-inondation-dos-rechargé / ).
  • La commande permet des recherches de plage efficaces, par exemple, itère sur tous les éléments avec une clé ≥ 42.
user1531083
la source
14

Les tables de hachage ont des constantes plus élevées que les implémentations de cartes courantes, qui deviennent importantes 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 que jamais, mais O (log n) est proche de O (k). (N'oubliez pas que la complexité logarithmique est toujours très bonne.)

Ce qui fait une bonne fonction de hachage dépend des caractéristiques de vos données; donc si je ne prévois pas de regarder une fonction de hachage personnalisée (mais je peux certainement changer d'avis plus tard, et facilement depuis que je tape typiquement près de tout) et même si les valeurs par défaut sont choisies pour fonctionner correctement pour de nombreuses sources de données, je trouve l'ordre la nature de la carte pour être assez utile au départ que je mets toujours par défaut à la carte 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 UDT), et à écrire simplement op <(que vous voulez quand même).


la source
@Roger, connaissez-vous la quantité approximative d'éléments auxquels correspond le mieux unordered_map? Je vais probablement écrire un test pour ça, de toute façon ... (+1)
Kornel Kisielewicz
1
@Kornel: Il n'en faut pas beaucoup; mes tests ont porté sur environ 10 000 éléments. Si nous voulons un graphique vraiment précis, vous pouvez regarder une implémentation de mapet une de unordered_map, avec une certaine plate-forme et une certaine taille de cache, et faire une analyse complexe. : P
GManNickG
Dépend des détails de l'implémentation, des paramètres de réglage au moment de la compilation (faciles à prendre en charge si vous écrivez votre propre implémentation) et même de la machine spécifique utilisée pour les tests. Tout comme pour les autres conteneurs, le comité fixe uniquement les exigences générales.
13

Des 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 le pire des cas O (log n). Les opérations std :: unordered_map (table de hachage) sont amorties O (1) et le pire des cas O (n).

En pratique, la table de hachage "hoquet" de temps en temps avec une opération O (n), ce qui peut ou non être quelque chose que votre application peut tolérer. S'il ne le tolère pas, vous préféreriez std :: map à std :: unordered_map.

Don Hatch
la source
12

Sommaire

En supposant que la commande n'est pas importante:

  • Si vous allez construire une grande table une fois et faire beaucoup de requêtes, utilisez std::unordered_map
  • Si vous allez construire une petite table (peut contenir moins de 100 éléments) et faire beaucoup de requêtes, utilisez std::map. C'est parce que les lectures le sont O(log n).
  • Si vous allez beaucoup changer de table, c'est peut-être une std::map bonne option.
  • En cas de doute, utilisez simplement std::unordered_map.

Contexte historique

Dans la plupart des langues, la carte non ordonnée (ou dictionnaires basés sur le hachage) est la carte par défaut, mais en C ++, vous obtenez la carte ordonnée comme carte par défaut. Comment est-ce arrivé? Certaines personnes supposent à tort que le comité C ++ a pris cette décision dans leur sagesse unique, mais la vérité est malheureusement plus laide que cela.

Il est largement admis que C ++ s'est retrouvé avec une carte ordonnée par défaut car il n'y a pas trop de paramètres sur la façon dont ils peuvent être implémentés. D'un autre côté, les implémentations basées sur le hachage ont des tonnes de choses à dire. Donc, pour éviter les blocages dans la normalisation, ils se sont juste entendus avec la carte ordonnée. Vers 2005, de nombreuses langues avaient déjà de bonnes implémentations d'implémentation basée sur le hachage et il était donc plus facile pour le comité d'accepter de nouvelles std::unordered_map. Dans un monde parfait, std::mapaurait été désordonné et nous aurions std::ordered_mapcomme type distinct.

Performance

Ci-dessous, deux graphiques devraient parler d'eux-mêmes ( source ):

entrez la description de l'image ici

entrez la description de l'image ici

Shital Shah
la source
Données intéressantes; combien de plateformes avez-vous inclus dans vos tests?
Toby Speight
1
pourquoi devrais-je utiliser std :: map pour une petite table lorsque je fais beaucoup de requêtes car std :: unordered_map fonctionne toujours mieux que std :: map selon les 2 images que vous avez publiées ici?
ricky
Le graphique montre les performances pour 0,13 million d'éléments ou plus. Si vous avez de petits éléments (peut être <100), alors O (log n) peut devenir plus petit qu'une carte non ordonnée.
Shital Shah
10

J'ai récemment fait un test qui fait 50000 fusionner et trier. Cela signifie que si les clés de chaîne sont identiques, fusionnez la chaîne d'octets. Et la sortie finale doit être triée. Cela inclut donc une recherche pour chaque insertion.

Pour la mapmise en œuvre, il faut 200 ms pour terminer le travail. Pour le unordered_map+ map, il faut 70 ms pour l' unordered_mapinsertion et 80 ms pour l' mapinsertion. L'implémentation hybride est donc 50 ms plus rapide.

Nous devrions réfléchir à deux fois avant d'utiliser le map. Si vous avez seulement besoin de trier les données dans le résultat final de votre programme, une solution hybride peut être meilleure.

wendong
la source
0

Petit ajout à tout ce qui précède:

Mieux utiliser map, lorsque vous avez besoin d'obtenir des éléments par plage, car ils sont triés et vous pouvez simplement les parcourir d'une frontière à l'autre.

Denis Sablukov
la source
-1

De: http://www.cplusplus.com/reference/map/map/

"En interne, les éléments d'une carte sont toujours triés par sa clé selon un critère de classement faible strict spécifique indiqué par son objet de comparaison interne (de type Compare).

les conteneurs de carte sont généralement plus lents que les conteneurs unordered_map pour accéder aux éléments individuels par leur clé, mais ils permettent l'itération directe sur les sous-ensembles en fonction de leur ordre. "

Kunal Bansal
la source