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); }
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
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) :
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
Résultat du test 1 :
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;
à :
uint64_t size = 1 << 20;
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 :
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;
changé en :
uint64_t size = 1 << 20;
Résultat :
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); }
Voici ce qu'elle fait Résultat en g :
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!