Wir entwickeln eine hochleistungskritische Software in C ++. Dort benötigen wir eine gleichzeitige Hash-Map und implementieren eine. Also haben wir einen Benchmark geschrieben, um herauszufinden, mit wie viel langsamer unsere gleichzeitige Hash-Map verglichen wird std::unordered_map
.
Aber es std::unordered_map
scheint unglaublich langsam zu sein ... Das ist also unser Mikro-Benchmark (für die gleichzeitige Karte haben wir einen neuen Thread erstellt, um sicherzustellen, dass das Sperren nicht wegoptimiert wird, und beachten Sie, dass ich niemals 0 einfüge, weil ich auch einen Benchmark mit google::dense_hash_map
, welches einen Nullwert benötigt):
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;
(BEARBEITEN: Den gesamten Quellcode finden Sie hier: http://pastebin.com/vPqf7eya )
Das Ergebnis für std::unordered_map
ist:
inserts: 35126
get : 2959
Für google::dense_map
:
inserts: 3653
get : 816
Für unsere handgestützte gleichzeitige Karte (die sperrt, obwohl der Benchmark Single-Threaded ist - aber in einem separaten Spawn-Thread):
inserts: 5213
get : 2594
Wenn ich das Benchmark-Programm ohne Pthread-Unterstützung kompiliere und alles im Hauptthread ausführe, erhalte ich die folgenden Ergebnisse für unsere handgestützte gleichzeitige Karte:
inserts: 4441
get : 1180
Ich kompiliere mit folgendem Befehl:
g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc
Daher std::unordered_map
scheinen besonders Einfügungen extrem teuer zu sein - 35 Sekunden gegenüber 3-5 Sekunden für andere Karten. Auch die Suchzeit scheint ziemlich hoch zu sein.
Meine Frage: Warum ist das so? Ich habe eine andere Frage zum Stackoverflow gelesen, in der jemand fragt, warum sie std::tr1::unordered_map
langsamer ist als seine eigene Implementierung. Dort gibt die am höchsten bewertete Antwort an, dass std::tr1::unordered_map
eine kompliziertere Schnittstelle implementiert werden muss. Aber ich kann dieses Argument nicht sehen: Wir verwenden einen Bucket-Ansatz in unserer concurrent_map, std::unordered_map
verwenden auch einen Bucket-Ansatz ( google::dense_hash_map
nicht, aber als std::unordered_map
sollte er mindestens so schnell sein wie unsere handunterstützte, parallelitätssichere Version?). Abgesehen davon kann ich in der Benutzeroberfläche nichts sehen, was eine Funktion erzwingt, die die Leistung der Hash-Map beeinträchtigt ...
Also meine Frage: Stimmt es, dass std::unordered_map
es sehr langsam zu sein scheint? Wenn nein: was ist los? Wenn ja: Was ist der Grund dafür?
Und meine Hauptfrage: Warum ist das Einfügen eines Werts in einen std::unordered_map
so schrecklich teuren Wert (selbst wenn wir zu Beginn genügend Platz reservieren, funktioniert er nicht viel besser - also scheint das Aufwärmen nicht das Problem zu sein)?
BEARBEITEN:
Zuallererst: Ja, der vorgestellte Benchmark ist nicht fehlerfrei - das liegt daran, dass wir viel damit uint64
herumgespielt haben und es nur ein Hack ist (zum Beispiel wäre die Verteilung zum Generieren von Ints in der Praxis keine gute Idee, 0 in einer Schleife auszuschließen ist irgendwie dumm etc ...).
Im Moment erklären die meisten Kommentare, dass ich die unordered_map schneller machen kann, indem ich genügend Speicherplatz dafür vorab zuweise. In unserer Anwendung ist dies einfach nicht möglich: Wir entwickeln ein Datenbankverwaltungssystem und benötigen eine Hash-Map, um einige Daten während einer Transaktion zu speichern (z. B. Sperren von Informationen). Diese Karte kann also alles von 1 (Benutzer macht nur eine Einfügung und Festschreibung) bis zu Milliarden von Einträgen (wenn vollständige Tabellenscans stattfinden) sein. Es ist einfach unmöglich, hier genügend Speicherplatz vorzuweisen (und am Anfang nur viel Speicherplatz zuzuweisen, verbraucht zu viel Speicher).
Außerdem entschuldige ich mich, dass ich meine Frage nicht klar genug formuliert habe: Ich bin nicht wirklich daran interessiert, unordered_map schnell zu machen (die Verwendung von Googles Densish Hash Map funktioniert gut für uns), ich verstehe nur nicht wirklich, woher diese enormen Leistungsunterschiede kommen . Es kann nicht nur eine Vorbelegung sein (selbst bei genügend vorbelegtem Speicher ist die dichte Karte eine Größenordnung schneller als unordered_map, unsere handgestützte gleichzeitige Karte beginnt mit einem Array der Größe 64 - also einem kleineren als unordered_map).
Was ist der Grund für diese schlechte Leistung von std::unordered_map
? Oder anders gefragt: Könnte man eine Implementierung der std::unordered_map
Schnittstelle schreiben , die standardkonform und (fast) so schnell wie Googles ist? Oder enthält der Standard etwas, das den Implementierer dazu zwingt, einen ineffizienten Weg zur Implementierung zu wählen?
EDIT 2:
Durch die Profilerstellung sehe ich, dass viel Zeit für ganzzahlige Divisionen verwendet wird. std::unordered_map
verwendet Primzahlen für die Arraygröße, während die anderen Implementierungen Zweierpotenzen verwenden. Warum std::unordered_map
werden Primzahlen verwendet? Um eine bessere Leistung zu erzielen, wenn der Hash schlecht ist? Für gute Hashes macht es imho keinen Unterschied.
EDIT 3:
Dies sind die Zahlen für std::map
:
inserts: 16462
get : 16978
Sooooooo: Warum sind Einfügungen in eine std::map
schneller als Einfügungen in eine std::unordered_map
... Ich meine WAT? std::map
hat eine schlechtere Lokalität (Baum gegen Array), muss mehr Zuordnungen vornehmen (pro Einfügung gegen pro Wiederaufbereitung + plus ~ 1 für jede Kollision) und, was am wichtigsten ist: hat eine andere algorithmische Komplexität (O (logn) gegen O (1))!
SIZE
.