Untersuchung der ungewöhnlichen Leistungsunterschiede zwischen u64-Schleifenzählern und _mm_popcnt_u64 auf x86-CPUs
Einführung
Ich suche nach einer schnellen Möglichkeit, Operationen an großen Datenfeldern durchzuführen popcount-Methode bin ich auf ein sehr seltsames Verhalten gestoßen: Das Ändern der Schleifenvariablen von unsigned in uint64_t führte zu einem Leistungsabfall von 50 % auf meinem PC.
Benchmark
#include <iostream> #include <chrono> #include <x86intrin.h> int main(int argc, char* argv[]) { using namespace std; if (argc != 2) { cerr << "usage: array_size in MB" << endl; return -1; } uint64_t size = atol(argv[1])<<20; uint64_t* buffer = new uint64_t[size/8]; char* charbuffer = reinterpret_cast<char*>(buffer); for (unsigned i=0; i<size; ++i) charbuffer[i] = rand()%256; uint64_t count,duration; chrono::time_point<chrono::system_clock> startP,endP; { startP = chrono::system_clock::now(); count = 0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with unsigned for (unsigned i=0; i<size/8; i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } { startP = chrono::system_clock::now(); count=0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with uint64_t for (uint64_t i=0;i<size/8;i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } free(charbuffer); }
Wie Sie sehen können, haben wir einen zufälligen Datenpuffer der Größe x MB erstellt, wobei x von der Befehlszeile gelesen wird. Anschließend iterieren wir über den Puffer und führen Popcount mithilfe einer entrollten Version des x86-Popcount-Intrinsic durch. Um genauere Ergebnisse zu erhalten, führen wir den Popcount 10.000 Mal durch. Die Zeit, in der wir die Popanzahl messen. Im ersten Fall ist die innere Schleifenvariable ohne Vorzeichen, im zweiten Fall ist die innere Schleifenvariable uint64_t. Ich dachte, das sollte keinen Unterschied machen, aber das tut es nicht.
(absolut verrücktes) Ergebnis
Ich habe es so kompiliert (g-Version: Ubuntu 4.8.2-19ubuntu1):
g++ -O3 -march=native -std=c++11 test.cpp -o test
Dies Ich habe den Test auf meiner Haswell Core i7-4770K CPU mit 3,50 GHz durchgeführt Ergebnis für 1 (also 1 MB Zufallsdaten):
Wie Sie sehen können, hat die uint64_t-Version den halben Durchsatz der unsignierten Version! Das Problem scheint zu sein, dass unterschiedliche Baugruppen generiert werden, aber was ist der Grund? Zuerst dachte ich, es sei ein Compiler-Fehler, also habe ich Clang (Ubuntu Clang Version 3.4-1ubuntu3) ausprobiert:
clang++ -O3 -march=native -std=c++11 teest.cpp -o test
Testergebnis 1:
Also fast das gleiche Ergebnis, immer noch seltsam. Aber jetzt wird es wirklich seltsam. Ich habe die aus der Eingabe gelesene Puffergröße durch eine Konstante 1 ersetzt, also habe ich von:
uint64_t size = atol(argv[1]) << 20;
zu:
uint64_t size = 1 << 20;
geändert, sodass der Compiler jetzt zur Kompilierungszeit die Puffergröße kennt. Vielleicht kann es einige Optimierungen hinzufügen! Hier sind die Zahlen in g:
Beide Versionen sind jetzt gleich schnell. Allerdings wird Velocidade im Vergleich zu Unsigned noch langsamer! Sie sank von 26 GB/s auf 20 GB/s, sodass das Ersetzen einer unkonventionellen Konstante durch einen konstanten Wert zu einer Deoptimierung führte. Im Ernst, ich habe hier keine Ahnung! Aber jetzt mit Clang und neuer Version:
uint64_t size = atol(argv[1]) << 20;
geändert zu:
uint64_t size = 1 << 20;
Ergebnis:
Moment, was ist passiert? Jetzt haben beide Versionen eine niedrige Geschwindigkeit von 15 GB/s. Das Ersetzen eines unkonventionellen konstanten Werts durch einen konstanten Wert führte also sogar dazu, dass zwei Versionen des Codes für Clang langsamer waren!
Ich habe einen Kollegen, der eine Ivy-Bridge-CPU verwendet, gebeten, meine Benchmarks zusammenzustellen. Er erzielte ähnliche Ergebnisse, daher scheint dies nicht nur bei Haswell der Fall zu sein. Da hier zwei Compiler seltsame Ergebnisse liefern, scheint es sich hier auch nicht um einen Compiler-Fehler zu handeln. Da wir hier keine AMD-CPU haben, können wir zum Testen nur Intel verwenden.
Mehr Verrücktheit, bitte!
Fügen Sie anhand des ersten Beispiels (das mit atol(argv[1])) eine statische Variable vor die Variable ein, d. h.:
#include <iostream> #include <chrono> #include <x86intrin.h> int main(int argc, char* argv[]) { using namespace std; if (argc != 2) { cerr << "usage: array_size in MB" << endl; return -1; } uint64_t size = atol(argv[1])<<20; uint64_t* buffer = new uint64_t[size/8]; char* charbuffer = reinterpret_cast<char*>(buffer); for (unsigned i=0; i<size; ++i) charbuffer[i] = rand()%256; uint64_t count,duration; chrono::time_point<chrono::system_clock> startP,endP; { startP = chrono::system_clock::now(); count = 0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with unsigned for (unsigned i=0; i<size/8; i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } { startP = chrono::system_clock::now(); count=0; for( unsigned k = 0; k < 10000; k++){ // Tight unrolled loop with uint64_t for (uint64_t i=0;i<size/8;i+=4) { count += _mm_popcnt_u64(buffer[i]); count += _mm_popcnt_u64(buffer[i+1]); count += _mm_popcnt_u64(buffer[i+2]); count += _mm_popcnt_u64(buffer[i+3]); } } endP = chrono::system_clock::now(); duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count(); cout << "uint64_t\t" << count << '\t' << (duration/1.0E9) << " sec \t" << (10000.0*size)/(duration) << " GB/s" << endl; } free(charbuffer); }
Hier ist, was sie ergibt Ergebnis in g:
Yay, es gibt noch eine Alternative! Mit u3 haben wir immer noch 32GB/s, aber wir haben es geschafft, u64 zumindest von der 13GB/s-Version auf die 20GB/s-Version zu bringen! Auf dem Computer meines Kollegen war die u64-Version sogar schneller als die u32-Version und lieferte die besten Ergebnisse. Leider funktioniert das nur mit g , Clang scheint sich nicht um statische Aufladung zu kümmern.
**Meine Frage
Das obige ist der detaillierte Inhalt vonWarum wirkt sich das Ändern eines Schleifenzählers von „unsigned' auf „uint64_t' erheblich auf die Leistung von „_mm_popcnt_u64' auf x86-CPUs aus und welche Auswirkungen haben Compileroptimierung und Variablendeklaration?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!