Heim > Backend-Entwicklung > C++ > Warum 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?

Warum 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?

Linda Hamilton
Freigeben: 2024-12-05 10:42:15
Original
892 Leute haben es durchsucht

Why does changing a loop counter from `unsigned` to `uint64_t` significantly impact the performance of `_mm_popcnt_u64` on x86 CPUs, and how does compiler optimization and variable declaration affect this performance difference?

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);
}
Nach dem Login kopieren
Nach dem Login kopieren

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
Nach dem Login kopieren

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):

  • unsigned 41959360000 0,401554 Sekunden 26,113 GB/s
  • uint64_t 41959360000 0,759822 Sekunden 13,8003 GB/s

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
Nach dem Login kopieren

Testergebnis 1:

  • unsigned 41959360000 0,398293 Sekunden 26,3267 GB/Sek.
  • uint64_t 41959360000 0,680954 Sek. 15,3986 GB/Sek.

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;
Nach dem Login kopieren
Nach dem Login kopieren

zu:

uint64_t size = 1 << 20;
Nach dem Login kopieren
Nach dem Login kopieren

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:

  • unsigned 41959360000 0,509156 Sekunden 20,5944 GB/s
  • uint64_t 41959360000 0,508673 Sekunden 20,6139 GB/s

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;
Nach dem Login kopieren
Nach dem Login kopieren

geändert zu:

uint64_t size = 1 << 20;
Nach dem Login kopieren
Nach dem Login kopieren

Ergebnis:

  • unsigned 41959360000 0,677009 Sek. 15,4884 GB/s
  • uint64_t 41959360000 0,676909 Sek. 15,4906 GB/s

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);
}
Nach dem Login kopieren
Nach dem Login kopieren

Hier ist, was sie ergibt Ergebnis in g:

  • unsigned 41959360000 0,396728 Sek. 26,4306 GB/Sek.
  • uint64_t 41959360000 0,509484 Sek. 20,5811 GB/Sek.

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!

Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage