Nous développons un logiciel très performant en C ++. Il nous faut une carte de hachage simultanée et une carte implémentée. Donc, nous avons écrit un test pour déterminer combien de temps notre carte de hachage simultanée est comparée avec std::unordered_map
.
Mais std::unordered_map
semble être incroyablement lent … Voici donc notre micro-test (pour la carte concurrente, nous avons créé un nouveau thread pour s’assurer que le locking n’est pas optimisé et que je n’insère jamais 0 car également référence avec google::dense_hash_map
, qui nécessite une valeur nulle):
boost::random::mt19937 rng; boost::random::uniform_int_dissortingbution dist(std::numeric_limits::min(), std::numeric_limits::max()); std::vector vec(SIZE); for (int i = 0; i < SIZE; ++i) { uint64_t val = 0; while (val == 0) { val = dist(rng); } vec[i] = val; } std::unordered_map map; auto begin = std::chrono::high_resolution_clock::now(); for (int i = 0; i < SIZE; ++i) { map[vec[i]] = 0.0; } auto end = std::chrono::high_resolution_clock::now(); auto elapsed = std::chrono::duration_cast(end - begin); std::cout << "inserts: " << elapsed.count() << std::endl; std::random_shuffle(vec.begin(), vec.end()); begin = std::chrono::high_resolution_clock::now(); long double val; for (int i = 0; i < SIZE; ++i) { val = map[vec[i]]; } end = std::chrono::high_resolution_clock::now(); elapsed = std::chrono::duration_cast(end - begin); std::cout << "get: " << elapsed.count() << std::endl;
(EDIT: le code source complet peut être trouvé ici: http://pastebin.com/vPqf7eya )
Le résultat pour std::unordered_map
est:
inserts: 35126 get : 2959
Pour google::dense_map
:
inserts: 3653 get : 816
Pour notre carte concurrente sauvegardée à la main (qui effectue le locking, bien que le benchmark soit mono-thread – mais dans un thread de spawn séparé):
inserts: 5213 get : 2594
Si je comstack le programme de référence sans le support de pthread et que je lance tout dans le thread principal, j’obtiens les résultats suivants pour notre carte concurrente:
inserts: 4441 get : 1180
Je comstack avec la commande suivante:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Donc, en particulier, les insertions sur std::unordered_map
semblent être extrêmement coûteuses – 35 secondes contre 3-5 secondes pour les autres cartes. De plus, le temps de consultation semble être assez élevé.
Ma question: pourquoi est-ce? J’ai lu une autre question sur stackoverflow où quelqu’un demande, pourquoi std::tr1::unordered_map
est plus lent que sa propre implémentation. La réponse la plus élevée indique que le std::tr1::unordered_map
doit implémenter une interface plus compliquée. Mais je ne peux pas voir cet argument: nous utilisons une approche par google::dense_hash_map
dans notre concurrent_map, std::unordered_map
utilise également une approche par google::dense_hash_map
( google::dense_hash_map
ne le fait pas, mais que std::unordered_map
devrait être au moins aussi rapide que notre main) version protégée contre la concurrence?). En dehors de cela, je ne vois rien dans l’interface qui force une fonctionnalité qui fait que la carte de hachage fonctionne mal …
Donc ma question: est-il vrai que std::unordered_map
semble être très lent? Si non: qu’est-ce qui ne va pas? Si oui: quelle en est la raison?
Et ma question principale: pourquoi insérer une valeur dans un std::unordered_map
si terrible (même si nous réservons suffisamment d’espace au début, cela ne donne pas de meilleurs résultats – alors le rehashing semble ne pas être le problème)?
Tout d’abord: oui, le benchmark présenté n’est pas sans défaut – c’est parce que nous avons beaucoup joué avec et que c’est juste un hack (par exemple la dissortingbution uint64
pour générer des ints ne serait pas une bonne idée, exclure 0 dans un la boucle est un peu stupide, etc …).
Pour le moment, la plupart des commentaires expliquent que je peux rendre unordered_map plus rapide en pré-allouant suffisamment d’espace pour cela. Dans notre application, cela n’est tout simplement pas possible: nous développons un système de gestion de firebase database et avons besoin d’une carte de hachage pour stocker certaines données lors d’une transaction (par exemple, des informations de locking). Donc, cette carte peut être tout de 1 (l’utilisateur ne fait qu’un seul insert et valide) à des milliards d’entrées (si des parsings de table complètes ont lieu). Il est tout simplement impossible de pré-allouer suffisamment d’espace ici (et il suffit d’allouer beaucoup au début pour consumr trop de mémoire).
De plus, je m’excuse, que je n’ai pas énoncé ma question assez clairement: je ne suis pas vraiment intéressé à rendre unordered_map rapide (utiliser des cartes de hachage denses googles fonctionne bien pour nous), je ne comprends pas vraiment d’où proviennent ces énormes différences . Il ne peut pas s’agir simplement d’une préallocation (même avec suffisamment de mémoire pré-allouée, la carte dense est un ordre de grandeur plus rapide que unordered_map, notre carte concurrente à la main commence par un tableau de taille 64 – donc plus petit que unordered_map).
Alors, quelle est la raison de cette mauvaise performance de std::unordered_map
? Ou différemment: Peut-on écrire une implémentation de l’interface std::unordered_map
qui est conforme à la norme et (presque) aussi rapide que la carte de hachage dense googles? Ou existe-t-il quelque chose dans la norme qui oblige le responsable de la mise en œuvre à choisir un moyen inefficace pour le mettre en œuvre?
En profilant, je constate que beaucoup de temps est utilisé pour les divions entières. std::unordered_map
utilise des nombres premiers pour la taille du tableau, tandis que les autres implémentations utilisent des puissances de deux. Pourquoi std::unordered_map
utilise-t-il des nombres premiers? Pour mieux performer si le hash est mauvais? Pour de bons hachages, cela ne fait aucune différence.
Ce sont les nombres pour std::map
:
inserts: 16462 get : 16978
Sooooooo: pourquoi les insertions dans un std::map
plus rapides que les insertions dans un std::unordered_map
… je veux dire WAT? std::map
a une plus mauvaise localité (tree vs array), doit faire plus d’allocations (par insert vs par rehash + plus ~ 1 pour chaque collision) et, plus important: a une autre complexité algorithmique (O (logn) vs O ( 1))!
J’ai trouvé la raison: c’est un problème de gcc-4.7 !!
Avec gcc-4.7
inserts: 37728 get : 2985
Avec gcc-4.6
inserts: 2531 get : 1565
Donc std::unordered_map
dans gcc-4.7 est cassé (ou mon installation, qui est une installation de gcc-4.7.0 sur Ubuntu – et une autre installation qui est gcc 4.7.1 sur les tests debian).
Je vais soumettre un rapport de bogue .. jusque-là: N’utilisez pas std::unordered_map
avec gcc 4.7!
Je suppose que vous n’avez pas correctement dimensionné votre unordered_map, comme l’a suggéré Ylisar. Lorsque les chaînes deviennent trop longues dans unordered_map
, l’implémentation de g ++ sera automatiquement relancée vers une plus grande table de hachage, ce qui affecterait considérablement les performances. Si je me souviens bien, unordered_map
défaut (plus petit nombre premier que 100
).
Je n’avais pas de chrono
sur mon système, alors j’ai chronométré avec le times()
.
template void time_test (TEST t, const char *m) { struct tms start; struct tms finish; long ticks_per_second; times(&start); t(); times(&finish); ticks_per_second = sysconf(_SC_CLK_TCK); std::cout << "elapsed: " << ((finish.tms_utime - start.tms_utime + finish.tms_stime - start.tms_stime) / (1.0 * ticks_per_second)) << " " << m << std::endl; }
J'ai utilisé une SIZE
de 10000000
, et j'ai dû changer les choses un peu pour ma version de boost
. Notez également que j'ai pré-dimensionné la table de hachage pour qu'elle corresponde à SIZE/DEPTH
, où DEPTH
est une estimation de la longueur de la chaîne de compartiments due à des collisions de hachage.
Edit: Howard me fait remarquer dans les commentaires que le facteur de charge maximum pour unordered_map
est 1
. Ainsi, le DEPTH
contrôle combien de fois le code sera ressaisi.
#define SIZE 10000000 #define DEPTH 3 std::vector vec(SIZE); boost::mt19937 rng; boost::uniform_int dist(std::numeric_limits ::min(), std::numeric_limits ::max()); std::unordered_map map(SIZE/DEPTH); void test_insert () { for (int i = 0; i < SIZE; ++i) { map[vec[i]] = 0.0; } } void test_get () { long double val; for (int i = 0; i < SIZE; ++i) { val = map[vec[i]]; } } int main () { for (int i = 0; i < SIZE; ++i) { uint64_t val = 0; while (val == 0) { val = dist(rng); } vec[i] = val; } time_test(test_insert, "inserts"); std::random_shuffle(vec.begin(), vec.end()); time_test(test_insert, "get"); }
Modifier:
J'ai modifié le code pour pouvoir changer DEPTH
plus facilement.
#ifndef DEPTH #define DEPTH 10000000 #endif
Ainsi, par défaut, la taille la plus mauvaise pour la table de hachage est choisie.
elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000 elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000 elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000 elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000 elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000 elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100 elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10 elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1
Ma conclusion est qu'il n'y a pas beaucoup de différence de performance significative pour une taille de table de hachage initiale autre que de la rendre égale à la totalité du nombre prévu d'insertions uniques. De plus, je ne vois pas la différence de performance de l'ordre de grandeur que vous observez.
J’ai exécuté votre code en utilisant un ordinateur 64 bits / AMD / 4 cœurs (2.1 GHz) et cela m’a donné les résultats suivants:
MinGW-W64 4.9.2:
Utiliser std :: unordered_map:
inserts: 9280 get: 3302
En utilisant std :: map:
inserts: 23946 get: 24824
VC 2015 avec tous les indicateurs d’optimisation que je connais:
Utiliser std :: unordered_map:
inserts: 7289 get: 1908
En utilisant std :: map:
inserts: 19222 get: 19711
Je n’ai pas testé le code en utilisant GCC mais je pense qu’il peut être comparable aux performances de VC, donc si c’est vrai, alors GCC 4.9 std :: unordered_map est toujours cassé.
[MODIFIER]
Donc, oui, comme quelqu’un l’a dit dans les commentaires, il n’y a aucune raison de penser que les performances de GCC 4.9.x seraient comparables à celles des VC. Lorsque j’aurai le changement, je testerai le code sur GCC.
Ma réponse est juste pour établir une sorte de base de connaissances pour d’autres réponses.