Nous développons un logiciel critique hautement performant en C ++. Là, nous avons besoin d'une carte de hachage simultanée et d'une mise en œuvre. Nous avons donc écrit un benchmark pour déterminer à quel point notre carte de hachage simultanée est plus lente que cellestd::unordered_map
.
Mais, std::unordered_map
semble être incroyablement lent ... C'est donc notre micro-benchmark (pour la carte simultanée, nous avons engendré un nouveau thread pour nous assurer que le verrouillage ne soit pas optimisé et notez que je n'insère jamais 0 car je compare également avec google::dense_hash_map
, qui a besoin d'une valeur nulle):
boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> 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<int, long double> 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<std::chrono::milliseconds>(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<std::chrono::milliseconds>(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 verrouille, bien que le benchmark soit à thread unique - mais dans un thread de spawn séparé):
inserts: 5213
get : 2594
Si je compile 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 simultanée sauvegardée à la main:
inserts: 4441
get : 1180
Je compile avec la commande suivante:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Alors surtout insère sur std::unordered_map
semblent donc être extrêmement coûteuses - 35 secondes contre 3-5 secondes pour les autres cartes. De plus, le temps de recherche 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. Là, la réponse la mieux notée indique que la std::tr1::unordered_map
nécessité d'implémenter une interface plus compliquée. Mais je ne vois pas cet argument: nous utilisons une approche bucket dans notre concurrent_map, std::unordered_map
utilise également une approche bucket (google::dense_hash_map
ne le fait pas, mais std::unordered_map
devrait être au moins aussi rapide que notre version sécurisée par la concurrence?). A part ça, 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 cher (même si nous réservons suffisamment d'espace au début, cela ne fonctionne pas beaucoup mieux - donc le ressassement ne semble pas être le problème)?
ÉDITER:
Tout d'abord: oui, le benchmark présenté n'est pas parfait - c'est parce que nous avons beaucoup joué avec et c'est juste un hack (par exemple le uint64
distribution pour générer des ints ne serait en pratique pas une bonne idée, exclure 0 dans une boucle est un peu stupide etc ...).
Pour le moment, la plupart des commentaires expliquent que je peux rendre la carte unordered_map plus rapide en préallouant suffisamment d'espace pour elle. Dans notre application, ce n'est tout simplement pas possible: nous développons un système de gestion de base de données et avons besoin d'une carte de hachage pour stocker certaines données lors d'une transaction (par exemple, des informations de verrouillage). Ainsi, cette carte peut être de 1 (l'utilisateur ne fait qu'une insertion et s'engage) à des milliards d'entrées (si des analyses complètes de la table se produisent). Il est tout simplement impossible de préallouer suffisamment d'espace ici (et en allouer beaucoup au début consommera trop de mémoire).
De plus, je m'excuse de ne pas avoir formulé ma question assez clairement: je ne suis pas vraiment intéressé à rendre unordered_map rapide (l'utilisation d'une carte de hachage dense de googles fonctionne bien pour nous), je ne comprends tout simplement pas vraiment d'où viennent ces énormes différences de performances . Cela ne peut pas être juste 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 simultanée sauvegardée à la main commence avec un tableau de taille 64 - donc un plus petit que unordered_map).
Alors, quelle est la raison de cette mauvaise performance de std::unordered_map
? Ou différemment demandé: pourrait-on écrire une implémentation dustd::unordered_map
interface qui soit conforme au standard et (presque) aussi rapide que la carte de hachage dense de Google? Ou y a-t-il quelque chose dans la norme qui oblige le réalisateur à choisir une manière inefficace de l'implémenter?
MODIFIER 2:
En profilant, je vois que beaucoup de temps est utilisé pour les divions entiers. 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-on des nombres premiers? Pour mieux fonctionner si le hachage est mauvais? Pour les bons hachages, cela ne fait aucune différence.
MODIFIER 3:
Voici les chiffres pour std::map
:
inserts: 16462
get : 16978
Sooooooo: pourquoi les inserts dans un std::map
plus rapide que les inserts dans un std::unordered_map
... je veux dire WAT? std::map
a une pire localité (arbre vs tableau), doit faire plus d'allocations (par insert vs par rehash + plus ~ 1 pour chaque collision) et, le plus important: a une autre complexité algorithmique (O (logn) vs O (1))!
SIZE
.