web-dev-qa-db-de.com

Ist die Implementierung von gcc std :: unordered_map langsam? Wenn ja warum?

Wir entwickeln eine leistungskritische Software in C++. Dort brauchen wir eine Concurrent Hash Map und haben eine implementiert. Also haben wir einen Benchmark geschrieben, um herauszufinden, wie viel langsamer unsere gleichzeitige Hash-Map im Vergleich zu std::unordered_map Ist.

Aber std::unordered_map Scheint unglaublich langsam zu sein ... Dies ist also unser Mikro-Benchmark (für die gleichzeitige Map haben wir einen neuen Thread erstellt, um sicherzustellen, dass das Sperren nicht aufgehoben wird, und zu beachten, dass ich niemals 0 einfüge weil ich auch mit google::dense_hash_map vergleiche, was 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 handunterstützte Concurrent Map (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 Haupt-Thread ausführe, erhalte ich die folgenden Ergebnisse für unsere handgestützte Concurrent Map:

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 scheinen Einfügungen auf std::unordered_map Extrem teuer zu sein - 35 Sekunden gegenüber 3-5 Sekunden für andere Karten. Auch die Nachschlagezeit scheint recht hoch zu sein.

Meine Frage: warum ist das so? Ich habe eine andere Frage zum Stackoverflow gelesen, in der jemand fragt, warum std::tr1::unordered_map Langsamer ist als seine eigene Implementierung. Dort gibt die bestbewertete Antwort an, dass das std::tr1::unordered_map Eine kompliziertere Schnittstelle implementieren muss. Aber ich kann dieses Argument nicht sehen: Wir verwenden einen Bucket-Ansatz in unserer concurrent_map, std::unordered_map Verwendet auch einen Bucket-Ansatz (google::dense_hash_map Nicht, aber dann sollte std::unordered_map Auf sein mindestens so schnell wie unsere handgesicherte Version?). Abgesehen davon kann ich in der Benutzeroberfläche nichts sehen, was eine Funktion erzwingt, die die Leistung der Hash-Karte beeinträchtigt ...

Also meine Frage: Stimmt es, dass std::unordered_map Sehr langsam zu sein scheint? Wenn nein: was ist falsch? Wenn ja, woran liegt das?.

Und meine Hauptfrage: Warum ist das Einfügen eines Werts in einen std::unordered_map So schrecklich teuer (auch wenn wir zu Beginn genügend Speicherplatz reservieren, ist die Leistung nicht viel besser - das Aufwärmen scheint also nicht das Problem zu sein)?

BEARBEITEN:

Zuallererst: Ja, der vorgestellte Benchmark ist nicht fehlerfrei - das liegt daran, dass wir viel damit gespielt haben und es nur ein Hack ist (zum Beispiel wäre die uint64 - Verteilung, um Ints zu generieren, in der Praxis keine gute Idee , ausschließen 0 in einer Schleife ist irgendwie dumm etc ...).

Im Moment erklären die meisten Kommentare, dass ich die unordered_map schneller machen kann, indem ich genug Platz dafür vorbelege. 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 schreibt fest) bis zu Milliarden von Einträgen sein (wenn vollständige Tabellenscans durchgeführt werden). Es ist einfach unmöglich, hier genügend Speicherplatz vorab zuzuweisen (und wenn Sie zu Beginn nur viel Speicher zuweisen, wird zu viel Speicher verbraucht).

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-dichter 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 um eine Größenordnung schneller als die ungeordnete_Karte, unsere handunterstützte gleichzeitige Karte beginnt mit einem Array der Größe 64 - also einer kleineren als die ungeordnete_Karte).

Was ist der Grund für diese schlechte Leistung von std::unordered_map? Oder anders gefragt: Könnte man eine Implementierung der Schnittstelle std::unordered_map Schreiben, die standardkonform und (fast) so schnell wie eine googles-dichte Hash-Map ist? Oder gibt es etwas im Standard, das den Implementierer dazu zwingt, einen ineffizienten Weg zu wählen, um ihn zu implementieren?

EDIT 2:

Durch die Profilerstellung sehe ich, dass für ganzzahlige Divisionen viel Zeit benötigt wird. std::unordered_map Verwendet Primzahlen für die Arraygröße, während die anderen Implementierungen Potenzen von zwei verwenden. Warum verwendet std::unordered_map Primzahlen? 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 werden Einfügungen in einen std::map Schneller als Einfügungen in einen std::unordered_map ... ich meine WAT? std::map Hat eine schlechtere Lokalität (Tree vs Array), muss mehr Zuordnungen vornehmen (pro Insert vs pro Rehash + plus ~ 1 für jede Kollision) und hat vor allem eine andere algorithmische Komplexität (O (logn) vs O (1))!

99
Markus Pilman

Ich habe den Grund gefunden: es ist ein Problem von gcc-4.7 !!

Mit gcc-4.7

inserts: 37728
get    : 2985

Mit gcc-4.6

inserts: 2531
get    : 1565

Also ist std::unordered_map In gcc-4.7 kaputt (oder meine Installation, die eine Installation von gcc-4.7.0 unter Ubuntu ist - und eine andere Installation, die gcc 4.7.1 unter debian testing ist).

Ich werde einen Fehlerbericht einreichen. Bis dahin: NIEMALS std::unordered_map Mit gcc 4.7 verwenden!

85
Markus Pilman

Ich vermute, dass Sie die Größe Ihres unordered_map Nicht richtig bemessen haben, wie Ylisar vorgeschlagen hat. Wenn Ketten in unordered_map Zu lang werden, wird die g ++ - Implementierung automatisch in eine größere Hash-Tabelle zurückgespeichert, was die Leistung erheblich beeinträchtigt. Wenn ich mich richtig erinnere, ist unordered_map Standardmäßig (kleinste Primzahl größer als) 100.

Ich hatte chrono nicht auf meinem System, also habe ich mit times() zeitlich festgelegt.

template <typename TEST>
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;
}

Ich habe ein SIZE von 10000000 Verwendet und musste die Dinge für meine Version von boost ein wenig ändern. Beachten Sie auch, dass ich die Hash-Tabelle so angepasst habe, dass sie mit SIZE/DEPTH Übereinstimmt, wobei DEPTH eine Schätzung der Länge der Bucket-Kette aufgrund von Hash-Kollisionen ist.

Edit: Howard weist mich in Kommentaren darauf hin, dass der maximale Auslastungsfaktor für unordered_map1 Ist. Das DEPTH steuert also, wie oft der Code erneut verarbeitet wird.

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> 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");
}

Bearbeiten:

Ich habe den Code geändert, damit ich DEPTH leichter ändern kann.

#ifndef DEPTH
#define DEPTH 10000000
#endif

Daher wird standardmäßig die schlechteste Größe für die Hash-Tabelle ausgewählt.

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

Mein Fazit ist, dass es für eine anfängliche Hash-Tabellengröße nur einen signifikanten Leistungsunterschied gibt, der der gesamten erwarteten Anzahl eindeutiger Einfügungen entspricht. Außerdem sehe ich keinen Leistungsunterschied in der Größenordnung, den Sie beobachten.

21
jxh

Ich habe Ihren Code mit einem 64-Bit/AMD/4-Kerne-Computer (2,1 GHz) ausgeführt und dabei die folgenden Ergebnisse erhalten:

MinGW-W64 4.9.2:

Verwenden von std :: unordered_map:

inserts: 9280 
get: 3302

Verwenden von std :: map:

inserts: 23946
get: 24824

VC 2015 mit allen mir bekannten Optimierungsflags:

Verwenden von std :: unordered_map:

inserts: 7289
get: 1908

Verwenden von std :: map:

inserts: 19222 
get: 19711

Ich habe den Code nicht mit GCC getestet, aber ich denke, dass er mit der Leistung von VC vergleichbar ist. Wenn dies zutrifft, dann ist GCC 4.9 std :: unordered_map Es ist immer noch kaputt.

[EDIT]

Also ja, wie jemand in den Kommentaren sagte, gibt es keinen Grund zu der Annahme, dass die Leistung von GCC 4.9.x mit der Leistung von VC) vergleichbar wäre. Wenn ich die Änderung habe, werde ich die testen Code auf GCC.

Meine Antwort ist nur, eine Art Wissensbasis für andere Antworten aufzubauen.

3
Christian Leon