Leistung der Hash-Tabelle, warum ist C ++ am langsamsten?

7

Lief ein einfacher Leistungstest auf Hash, es scheint, dass C ++ Version langsamer als die Perl-Version und die Golang-Version ist.

  • Perl-Version dauerte etwa 200 Millisekunden,
  • C ++ Version dauerte 280 Millisekunden.
  • Golang-Version dauerte 56 Millisekunden.

Auf meinem PC mit Core (TM) i7-2670QM CPU @ 2.20GHz, Ubuntu 14.04.3LTS,

Irgendwelche Ideen?

Perl-Version

%Vor%

C ++ Version

%Vor%

Golang-Version

%Vor%

Update 1

Um die C ++ - Version zu verbessern, wurde x = mymap["China"]; in mymap["China"]; geändert, aber die Leistung ist sehr gering.

Update 2

Ich habe das ursprüngliche Ergebnis beim Kompilieren ohne Optimierung erhalten: g++ -std=c++11 unorderedMap.cc . Mit "-O2" -Optimierung kostet es nur etwa die Hälfte der Zeit (150ms)

Update 3

Um den möglichen char* to string -Konstruktoraufruf zu entfernen, habe ich eine String-Konstante erstellt. Die Zeit beträgt etwa 220ms (keine Optimierung beim Kompilieren). Dank des Vorschlags von @ neil-kirk, mit Optimierung (-O2-Flag) beträgt die Zeit ca. 80ms.

%Vor%

Update 4

Danke an @ steffen-ullrich, der darauf hingewiesen hat, dass es einen Syntaxfehler für die Perl-Version gibt. Ich habe es geändert. Die Performance-Nummer beträgt ca. 150ms.

Aktualisieren Sie 5

Es scheint, dass die Anzahl der ausgeführten Anweisungen zählt. Verwenden Sie den Befehl valgrind --tool=cachegrind <cmd>

Für die Go-Version

%Vor%

Für C ++ optimierte Version (kein Optimierungs-Flag)

%Vor%

Für C ++ optimierte Version

%Vor%     
packetie 27.11.2015, 05:03
quelle

4 Antworten

15

Aus Ihrem Perl-Code (vor Ihren Versuchen, es zu beheben):

%Vor%

Dies ist keine Karte, sondern ein Array. Die Syntax für Hash-Maps lautet:

%Vor%

Sie erstellen also effektiv ein Array und keine Hash-Map und greifen ständig auf das Element 0 zu. Bitte benutze use strict; und use warnings; die ganze Zeit mit Perl und schon eine einfache Syntaxprüfung mit Warnungen hätte dir das Problem gezeigt:

%Vor%

Abgesehen davon macht der Hauptteil des Benchmarks effektiv nichts Sinnvolles (weise eine Variable zu und benutze sie nie) und einige Compiler können sie erkennen und einfach optimieren.

Wenn Sie den von Ihrem Perl-Programm erzeugten Code überprüfen würden, würden Sie sehen:

%Vor%

Das heißt, das Problem wird zur Kompilierzeit erkannt und durch einen Zugriff auf den Array-Index 0 ersetzt.

Also wann immer Sie Benchmarks benötigen:

  • Überprüfen Sie, ob Ihr Programm tatsächlich das tut, was es soll.
  • Überprüfen Sie, ob der Compiler die Dinge nicht optimiert und nicht zur Kompilierungszeit ausführt, wenn andere Sprachen dies zur Laufzeit tun. Jede Art von Aussagen, die keine oder vorhersehbare Ergebnisse haben, sind anfällig für solche Optimierungen.
  • Vergewissern Sie sich, dass Sie tatsächlich messen, was Sie messen möchten, und nicht etwas anderes. Selbst kleine Änderungen am Programm können sich auf die Laufzeit auswirken, da eine Speicherzuweisung benötigt wird, die vorher nicht vorhanden war usw. und diese Änderungen sind möglicherweise nicht mit dem verbunden, was Sie messen möchten. In Ihrem Benchmark messen Sie den Zugriff auf das gleiche Hash-Element immer wieder ohne Zugriff auf andere Elemente dazwischen. Dies ist eine Aktivität, die sehr gut mit Prozessor-Caches spielen kann, aber wahrscheinlich nicht den tatsächlichen Gebrauch widerspiegelt.

Und die Verwendung eines einfachen Timers ist kein realistischer Maßstab. Es gibt andere Prozesse auf dem System, es gibt den Scheduler, es gibt Cache-Trashing ... und bei der heutigen CPU hängt es stark von der Belastung des Systems ab, weil vielleicht die CPU einen Benchmark in einem niedrigeren Leistungsmodus als die anderen Benchmarks ausführt , dh mit einem anderen CPU-Takt. Zum Beispiel variieren mehrere Läufe der gleichen "Benchmark" in der gemessenen Zeit zwischen 100ms und 150ms auf meinem System.

Benchmarks sind Lügen und Mikro-Benchmarks wie Ihres doppelt so.

    
Steffen Ullrich 27.11.2015 06:21
quelle
4

Ich habe Ihr Beispiel ein wenig modifiziert, um einige Details über die Struktur der Hash-Tabelle zu erhalten:

%Vor%

Wird dies auf meinem System ausgeführt, bekomme ich eine Laufzeit von ~ 68ms mit folgender Ausgabe:

%Vor%

Es stellt sich heraus, dass die Hash-Tabelle nicht gut optimiert ist und einige Kollisionen enthält. Ein weiteres Drucken der Elemente im Bucket zeigt, dass sich Spanien und China in Bucket 9 befinden. Der Bucket ist wahrscheinlich eine verknüpfte Liste mit Knoten, die im Speicher verteilt sind, und erklärt den Leistungsabfall.

Wenn Sie eine andere Größe der Hash-Tabelle wählen, so dass keine Kollisionen auftreten, können Sie eine Beschleunigung erzielen. Ich habe es mit mymap.rehash(1001) getestet und habe eine Beschleunigung von 20-30% auf etwas zwischen 44-52ms bekommen.

Nun wäre ein weiterer Punkt die Berechnung der Hash-Werte für "China". Die Funktion wird in jeder Iteration ausgeführt. Wir können dies verschwinden lassen, wenn wir zu konstanten einfachen C-Strings wechseln:

%Vor%

Auf meinem Rechner reduziert dies die Laufzeit um 50% auf ~ 20ms. Der Unterschied besteht darin, dass der Hash-Wert nicht mehr aus dem String-Inhalt berechnet wird, sondern nur die Adresse in einen Hash-Wert umgewandelt wird, der viel schneller ist, da er den Adresswert einfach als size_t zurückgibt. Wir müssen auch nicht erneut hashen, da es keine Kollisionen für den Bucket mit cn gibt.

    
Jens 27.11.2015 10:44
quelle
3

Dies zeigt nur, dass Go Hash Map Implementierung für diesen speziellen Anwendungsfall sehr gut optimiert ist.

mymap["China"] ruft mapaccess1_faststr auf, das speziell für Strings optimiert wurde Schlüssel. Insbesondere für kleine Ein-Bucket-Maps wird der Hash-Code nicht einmal für kurze Strings (weniger als 32 Bytes) berechnet.

    
kostya 27.11.2015 07:14
quelle
2

Das ist eine Vermutung:

unordered_map :: operator [] benötigt ein String-Argument. Sie stellen ein char * zur Verfügung. Ohne Optimierungen aktiviert die C ++ - Version den std :: string (char *) -Konstruktor millionenfach, um "China" in eine std :: string zu verwandeln. Die Sprachspezifikation von Go macht wahrscheinlich "String-Literale" vom gleichen Typ wie string, so dass keine Konstruktoren aufgerufen werden müssen.

Wenn Optimierungen aktiviert sind, wird der String-Konstruktor aus der Schleife geholt und Sie sehen nicht das gleiche Problem. Oder es wird höchstwahrscheinlich kein Code erzeugt, außer den beiden Systemaufrufen, um die Uhrzeit zu erhalten, und dem Systemaufruf, um die Differenz auszudrucken, denn letztendlich hat alles keinen Effekt.

Um das zu bestätigen, müssen Sie sich tatsächlich ansehen, welche Assembly generiert wird. Das wird eine Compileroption sein. Siehe diese Frage für die Flaggen erforderlich für GCC.

    
James Picone 27.11.2015 05:22
quelle

Tags und Links