Maison > développement back-end > C++ > Pourquoi le changement d'un compteur de boucles de « non signé » à « uint64_t » a-t-il un impact significatif sur les performances de « _mm_popcnt_u64 » sur les processeurs x86, et comment l'optimisation du compilateur et la déclaration de variable affectent-elles

Pourquoi le changement d'un compteur de boucles de « non signé » à « uint64_t » a-t-il un impact significatif sur les performances de « _mm_popcnt_u64 » sur les processeurs x86, et comment l'optimisation du compilateur et la déclaration de variable affectent-elles

Linda Hamilton
Libérer: 2024-12-05 10:42:15
original
931 Les gens l'ont consulté

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?

Exploration des différences de performances inhabituelles entre les compteurs de boucles u64 et _mm_popcnt_u64 sur les processeurs x86

Introduction

Je recherche un moyen rapide d'effectuer des opérations sur de grands tableaux de données popcount, j'ai rencontré un comportement très étrange : changer la variable de boucle de non signé à uint64_t a provoqué une baisse de performances de 50 % sur mon 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);
}
Copier après la connexion
Copier après la connexion

Comme vous pouvez le voir, nous avons créé un tampon de données aléatoires de taille x Mo, où x est lu à partir de la ligne de commande. Nous parcourons ensuite le tampon et effectuons un popcount en utilisant une version déroulée du popcount intrinsèque x86. Pour obtenir des résultats plus précis, nous effectuons un popcount 10 000 fois. L’heure à laquelle nous mesurons le nombre de personnes. Dans le premier cas, la variable de boucle interne n'est pas signée, dans le second cas, la variable de boucle interne est uint64_t. Je pensais que cela ne devrait faire aucune différence, mais ce n'est pas le cas.

résultat (absolument fou)

Je l'ai compilé comme ceci (version g : Ubuntu 4.8.2-19ubuntu1) :

g++ -O3 -march=native -std=c++11 test.cpp -o test
Copier après la connexion

Ceci J'ai effectué le test sur mon processeur Haswell Core i7-4770K à 3,50 GHz Résultat pour 1 (donc 1 Mo de données aléatoires) :

  • unsigned 41959360000 0,401554 secondes 26,113 Go/sec
  • uint64_t 41959360000 0,759822 secondes 13,8003 Go/sec

Comme vous pouvez le constater, la version uint64_t a la moitié du débit de la version non signée ! Le problème semble être que différents assemblages sont générés, mais quelle en est la raison ? Tout d'abord, je pensais que c'était un bug du compilateur, alors j'ai essayé clang (Ubuntu Clang version 3.4-1ubuntu3) :

clang++ -O3 -march=native -std=c++11 teest.cpp -o test
Copier après la connexion

Résultat du test 1 :

  • non signé 41959360000 0,398293 Secondes 26,3267 Go/sec
  • uint64_t 41959360000 0,680954 sec 15,3986 Go/sec

Donc, obtenir presque le même résultat, toujours bizarre. Mais maintenant, ça devient vraiment bizarre. J'ai remplacé la taille du tampon lue depuis l'entrée par une constante 1, j'ai donc changé de :

uint64_t size = atol(argv[1]) << 20;
Copier après la connexion
Copier après la connexion

à :

uint64_t size = 1 << 20;
Copier après la connexion
Copier après la connexion

Ainsi, le compilateur connaît désormais au moment de la compilation la taille du tampon. Peut-être que cela peut ajouter quelques optimisations ! Voici les nombres en g :

  • non signé 41959360000 0,509156 secondes 20,5944 Go/sec
  • uint64_t 41959360000 0,508673 secondes 20,6139 Go/sec

Les deux versions sont désormais tout aussi rapides. Cependant, la vitesse devient encore plus lente que celle non signée ! Il est passé de 26 Go/s à 20 Go/s, donc le remplacement d'une constante non conventionnelle par une valeur constante a entraîné une dé-optimisation. Sérieusement, je n'en ai aucune idée ici ! Mais maintenant avec clang et nouvelle version :

uint64_t size = atol(argv[1]) << 20;
Copier après la connexion
Copier après la connexion

changé en :

uint64_t size = 1 << 20;
Copier après la connexion
Copier après la connexion

Résultat :

  • non signé 41959360000 0,677009 sec 15,4884 Go/s
  • uint64_t 41959360000 0,676909 sec 15,4906 GB/s

Attendez, que s'est-il passé ? Désormais, les deux versions sont réduites à une vitesse faible de 15 Go/s. Ainsi, le remplacement d'une valeur constante non conventionnelle par une valeur constante a même entraîné deux versions du code plus lentes pour Clang !

J'ai demandé à un collègue qui utilise un processeur Ivy Bridge de compiler mes benchmarks. Il a obtenu des résultats similaires, donc cela ne semble pas être unique à Haswell. Étant donné que deux compilateurs produisent ici des résultats étranges, cela ne semble pas non plus être un bug du compilateur. Comme nous n'avons pas de processeur AMD ici, nous ne pouvons utiliser Intel que pour les tests.

Encore plus de folie, s'il vous plaît !

En utilisant le premier exemple (celui avec atol(argv[1])), mettez un static devant la variable, soit :

#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);
}
Copier après la connexion
Copier après la connexion

Voici ce qu'elle fait Résultat en g :

  • non signé 41959360000 0,396728 sec 26,4306 Go/s
  • uint64_t 41959360000 0,509484 sec 20,5811 Go/s

Ouais, il y a une autre alternative ! Nous avons encore 32 Go/s avec u3, mais nous avons réussi à faire passer u64 au moins de la version 13 Go/s à la version 20 Go/s ! Sur l'ordinateur de mon collègue, la version u64 était encore plus rapide que la version u32, donnant les meilleurs résultats. Malheureusement, cela ne fonctionne qu'avec g , clang ne semble pas se soucier de la statique.

**Ma question

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal