Comment obtenir les performances de pointe théoriques de 4 opérations en virgule flottante (double précision) par cycle sur un processeur Intel x86-64 moderne?
Autant que je sache, il faut trois cycles pour un SSEadd
et cinq cycles pour un mul
sur la plupart des processeurs Intel modernes (voir, par exemple - "Tableaux d'instructions" d'Agner Fog ). Grâce au pipeline, on peut obtenir un débit d'un add
par cycle si l'algorithme a au moins trois sommations indépendantes. Etant donné que cela est vrai pour les variables addpd
ainsi que les registres scalaires addsd
et SSE peuvent contenir deux double
, le débit peut atteindre deux flops par cycle. .
De plus, il semble (bien que je n’aie pas vu de documentation appropriée à ce sujet) que add
et mul
puissent être exécutés en parallèle, ce qui donne un débit maximum théorique de quatre bascules par cycle.
Cependant, je n'ai pas été en mesure de reproduire ces performances avec un simple programme C/C++. Ma meilleure tentative a abouti à environ 2,7 flops/cycle. Si quelqu'un peut contribuer à un simple programme C/C++ ou assembleur, il présente des performances optimales qui seront grandement appréciées.
Ma tentative:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>
double stoptime(void) {
struct timeval t;
gettimeofday(&t,NULL);
return (double) t.tv_sec + t.tv_usec/1000000.0;
}
double addmul(double add, double mul, int ops){
// Need to initialise differently otherwise compiler might optimise away
double sum1=0.1, sum2=-0.1, sum3=0.2, sum4=-0.2, sum5=0.0;
double mul1=1.0, mul2= 1.1, mul3=1.2, mul4= 1.3, mul5=1.4;
int loops=ops/10; // We have 10 floating point operations inside the loop
double expected = 5.0*add*loops + (sum1+sum2+sum3+sum4+sum5)
+ pow(mul,loops)*(mul1+mul2+mul3+mul4+mul5);
for (int i=0; i<loops; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
return sum1+sum2+sum3+sum4+sum5+mul1+mul2+mul3+mul4+mul5 - expected;
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("usage: %s <num>\n", argv[0]);
printf("number of operations: <num> millions\n");
exit(EXIT_FAILURE);
}
int n = atoi(argv[1]) * 1000000;
if (n<=0)
n=1000;
double x = M_PI;
double y = 1.0 + 1e-8;
double t = stoptime();
x = addmul(x, y, n);
t = stoptime() - t;
printf("addmul:\t %.3f s, %.3f Gflops, res=%f\n", t, (double)n/t/1e9, x);
return EXIT_SUCCESS;
}
Compilé avec
g++ -O2 -march=native addmul.cpp ; ./a.out 1000
produit la sortie suivante sur un processeur Intel Core i5-750, 2,66 GHz.
addmul: 0.270 s, 3.707 Gflops, res=1.326463
C'est-à-dire environ 1,4 flop par cycle. Regarder le code assembleur avec g++ -S -O2 -march=native -masm=intel addmul.cpp
la boucle principale me semble plutôt optimal:
.L4:
inc eax
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
mulsd xmm5, xmm3
mulsd xmm1, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
addsd xmm10, xmm2
addsd xmm9, xmm2
cmp eax, ebx
jne .L4
Changer les versions scalaires avec des versions compactées (addpd
et mulpd
) doublerait le nombre de flop sans changer le temps d'exécution, de sorte que j'aurais un peu moins de 2,8 flop par cycle. Existe-t-il un exemple simple permettant d'obtenir quatre flops par cycle?
Beau petit programme de Mysticial; voici mes résultats (seulement quelques secondes):
gcc -O2 -march=nocona
: 5,6 Gflops sur 10,66 Gflops (2.1 flops/cycle)cl /O2
, openmp supprimé: 10,1 Gflops sur 10,66 Gflops (3,8 flops/cycle)Tout cela semble un peu complexe, mais mes conclusions à ce jour sont les suivantes:
gcc -O2
change l'ordre des opérations indépendantes en virgule flottante dans le but d'alterner les addpd
et mulpd
si possible. La même chose s'applique à gcc-4.6.2 -O2 -march=core2
.
gcc -O2 -march=nocona
semble conserver l'ordre des opérations en virgule flottante tel que défini dans la source C++.
cl /O2
, le compilateur 64 bits de SDK pour Windows 7 effectue automatiquement le déroulement de la boucle et semble essayer d'arranger les opérations de sorte que des groupes de trois addpd
alternent avec trois mulpd
(enfin, du moins sur mon système et pour mon programme simple).
Mon Core i5 75 ( architecture Nehalem ) n'aime pas alterner les add et les mul et semble incapable d'exécuter les deux opérations en parallèle. Cependant, si regroupé dans 3, cela fonctionne soudainement comme par magie.
D'autres architectures (éventuellement Sandy Bridge et d'autres) semblent pouvoir exécuter add/mul en parallèle sans problème si elles alternent dans le code d'assemblage.
Bien que difficile à admettre, mais sur mon système, cl /O2
effectue beaucoup mieux les opérations d’optimisation de bas niveau pour mon système et réalise des performances proches du maximum pour le petit exemple C++ ci-dessus. J'ai mesuré entre 1,85-2,01 flops/cycle (j'ai utilisé clock () dans Windows, ce qui n'est pas très précis. Je suppose qu'il faut utiliser une meilleure minuterie - merci Mackie Messer).
Le meilleur que j'ai réussi avec gcc
était de dérouler manuellement la boucle et d'organiser les additions et les multiplications par groupes de trois. Avec g++ -O2 -march=nocona addmul_unroll.cpp
j'obtiens au mieux 0.207s, 4.825 Gflops
qui correspond à 1,8 flop/cycle et dont je suis assez satisfait.
Dans le code C++, j'ai remplacé la boucle for
par
for (int i=0; i<loops/3; i++) {
mul1*=mul; mul2*=mul; mul3*=mul;
sum1+=add; sum2+=add; sum3+=add;
mul4*=mul; mul5*=mul; mul1*=mul;
sum4+=add; sum5+=add; sum1+=add;
mul2*=mul; mul3*=mul; mul4*=mul;
sum2+=add; sum3+=add; sum4+=add;
mul5*=mul; mul1*=mul; mul2*=mul;
sum5+=add; sum1+=add; sum2+=add;
mul3*=mul; mul4*=mul; mul5*=mul;
sum3+=add; sum4+=add; sum5+=add;
}
Et l'Assemblée ressemble maintenant à
.L4:
mulsd xmm8, xmm3
mulsd xmm7, xmm3
mulsd xmm6, xmm3
addsd xmm13, xmm2
addsd xmm12, xmm2
addsd xmm11, xmm2
mulsd xmm5, xmm3
mulsd xmm1, xmm3
mulsd xmm8, xmm3
addsd xmm10, xmm2
addsd xmm9, xmm2
addsd xmm13, xmm2
...
J'ai déjà fait cette tâche auparavant. Mais c'était principalement pour mesurer la consommation d'énergie et les températures du processeur. Le code suivant (assez long) permet d’atteindre l’optimum sur mon Core i7 2600K.
L'essentiel à noter ici est la quantité massive de dérouleurs de boucles manuels ainsi que l'entrelacement de multiplications et d'ajouts ...
Le projet complet est disponible sur mon GitHub: https://github.com/Mysticial/Flops
Si vous décidez de le compiler et de l'exécuter, faites attention à la température de votre CPU !!!
Assurez-vous de ne pas surchauffer. Et assurez-vous que la limitation du processeur n'affecte pas vos résultats!
De plus, je n'assume aucune responsabilité pour les dommages pouvant résulter de l'exécution de ce code.
Notes:
#include <emmintrin.h>
#include <omp.h>
#include <iostream>
using namespace std;
typedef unsigned long long uint64;
double test_dp_mac_SSE(double x,double y,uint64 iterations){
register __m128d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF;
// Generate starting data.
r0 = _mm_set1_pd(x);
r1 = _mm_set1_pd(y);
r8 = _mm_set1_pd(-0.0);
r2 = _mm_xor_pd(r0,r8);
r3 = _mm_or_pd(r0,r8);
r4 = _mm_andnot_pd(r8,r0);
r5 = _mm_mul_pd(r1,_mm_set1_pd(0.37796447300922722721));
r6 = _mm_mul_pd(r1,_mm_set1_pd(0.24253562503633297352));
r7 = _mm_mul_pd(r1,_mm_set1_pd(4.1231056256176605498));
r8 = _mm_add_pd(r0,_mm_set1_pd(0.37796447300922722721));
r9 = _mm_add_pd(r1,_mm_set1_pd(0.24253562503633297352));
rA = _mm_sub_pd(r0,_mm_set1_pd(4.1231056256176605498));
rB = _mm_sub_pd(r1,_mm_set1_pd(4.1231056256176605498));
rC = _mm_set1_pd(1.4142135623730950488);
rD = _mm_set1_pd(1.7320508075688772935);
rE = _mm_set1_pd(0.57735026918962576451);
rF = _mm_set1_pd(0.70710678118654752440);
uint64 iMASK = 0x800fffffffffffffull;
__m128d MASK = _mm_set1_pd(*(double*)&iMASK);
__m128d vONE = _mm_set1_pd(1.0);
uint64 c = 0;
while (c < iterations){
size_t i = 0;
while (i < 1000){
// Here's the meat - the part that really matters.
r0 = _mm_mul_pd(r0,rC);
r1 = _mm_add_pd(r1,rD);
r2 = _mm_mul_pd(r2,rE);
r3 = _mm_sub_pd(r3,rF);
r4 = _mm_mul_pd(r4,rC);
r5 = _mm_add_pd(r5,rD);
r6 = _mm_mul_pd(r6,rE);
r7 = _mm_sub_pd(r7,rF);
r8 = _mm_mul_pd(r8,rC);
r9 = _mm_add_pd(r9,rD);
rA = _mm_mul_pd(rA,rE);
rB = _mm_sub_pd(rB,rF);
r0 = _mm_add_pd(r0,rF);
r1 = _mm_mul_pd(r1,rE);
r2 = _mm_sub_pd(r2,rD);
r3 = _mm_mul_pd(r3,rC);
r4 = _mm_add_pd(r4,rF);
r5 = _mm_mul_pd(r5,rE);
r6 = _mm_sub_pd(r6,rD);
r7 = _mm_mul_pd(r7,rC);
r8 = _mm_add_pd(r8,rF);
r9 = _mm_mul_pd(r9,rE);
rA = _mm_sub_pd(rA,rD);
rB = _mm_mul_pd(rB,rC);
r0 = _mm_mul_pd(r0,rC);
r1 = _mm_add_pd(r1,rD);
r2 = _mm_mul_pd(r2,rE);
r3 = _mm_sub_pd(r3,rF);
r4 = _mm_mul_pd(r4,rC);
r5 = _mm_add_pd(r5,rD);
r6 = _mm_mul_pd(r6,rE);
r7 = _mm_sub_pd(r7,rF);
r8 = _mm_mul_pd(r8,rC);
r9 = _mm_add_pd(r9,rD);
rA = _mm_mul_pd(rA,rE);
rB = _mm_sub_pd(rB,rF);
r0 = _mm_add_pd(r0,rF);
r1 = _mm_mul_pd(r1,rE);
r2 = _mm_sub_pd(r2,rD);
r3 = _mm_mul_pd(r3,rC);
r4 = _mm_add_pd(r4,rF);
r5 = _mm_mul_pd(r5,rE);
r6 = _mm_sub_pd(r6,rD);
r7 = _mm_mul_pd(r7,rC);
r8 = _mm_add_pd(r8,rF);
r9 = _mm_mul_pd(r9,rE);
rA = _mm_sub_pd(rA,rD);
rB = _mm_mul_pd(rB,rC);
i++;
}
// Need to renormalize to prevent denormal/overflow.
r0 = _mm_and_pd(r0,MASK);
r1 = _mm_and_pd(r1,MASK);
r2 = _mm_and_pd(r2,MASK);
r3 = _mm_and_pd(r3,MASK);
r4 = _mm_and_pd(r4,MASK);
r5 = _mm_and_pd(r5,MASK);
r6 = _mm_and_pd(r6,MASK);
r7 = _mm_and_pd(r7,MASK);
r8 = _mm_and_pd(r8,MASK);
r9 = _mm_and_pd(r9,MASK);
rA = _mm_and_pd(rA,MASK);
rB = _mm_and_pd(rB,MASK);
r0 = _mm_or_pd(r0,vONE);
r1 = _mm_or_pd(r1,vONE);
r2 = _mm_or_pd(r2,vONE);
r3 = _mm_or_pd(r3,vONE);
r4 = _mm_or_pd(r4,vONE);
r5 = _mm_or_pd(r5,vONE);
r6 = _mm_or_pd(r6,vONE);
r7 = _mm_or_pd(r7,vONE);
r8 = _mm_or_pd(r8,vONE);
r9 = _mm_or_pd(r9,vONE);
rA = _mm_or_pd(rA,vONE);
rB = _mm_or_pd(rB,vONE);
c++;
}
r0 = _mm_add_pd(r0,r1);
r2 = _mm_add_pd(r2,r3);
r4 = _mm_add_pd(r4,r5);
r6 = _mm_add_pd(r6,r7);
r8 = _mm_add_pd(r8,r9);
rA = _mm_add_pd(rA,rB);
r0 = _mm_add_pd(r0,r2);
r4 = _mm_add_pd(r4,r6);
r8 = _mm_add_pd(r8,rA);
r0 = _mm_add_pd(r0,r4);
r0 = _mm_add_pd(r0,r8);
// Prevent Dead Code Elimination
double out = 0;
__m128d temp = r0;
out += ((double*)&temp)[0];
out += ((double*)&temp)[1];
return out;
}
void test_dp_mac_SSE(int tds,uint64 iterations){
double *sum = (double*)malloc(tds * sizeof(double));
double start = omp_get_wtime();
#pragma omp parallel num_threads(tds)
{
double ret = test_dp_mac_SSE(1.1,2.1,iterations);
sum[omp_get_thread_num()] = ret;
}
double secs = omp_get_wtime() - start;
uint64 ops = 48 * 1000 * iterations * tds * 2;
cout << "Seconds = " << secs << endl;
cout << "FP Ops = " << ops << endl;
cout << "FLOPs = " << ops / secs << endl;
double out = 0;
int c = 0;
while (c < tds){
out += sum[c++];
}
cout << "sum = " << out << endl;
cout << endl;
free(sum);
}
int main(){
// (threads, iterations)
test_dp_mac_SSE(8,10000000);
system("pause");
}
Sortie (1 thread, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 - Version x64:
Seconds = 55.5104
FP Ops = 960000000000
FLOPs = 1.7294e+010
sum = 2.22652
La machine est un Core i7 2600K à 4,4 GHz. Théorie théorique SSE est de 4 flops * 4,4 GHz = 17,6 GFlops . Ce code atteint 17.3 GFlops - pas mal.
Sortie (8 threads, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 - Version x64:
Seconds = 117.202
FP Ops = 7680000000000
FLOPs = 6.55279e+010
sum = 17.8122
Théorie théorique SSE est de 4 flops * 4 cœurs * 4,4 GHz = 70,4 GFlops. La valeur réelle est ) 65,5 GFlops .
#include <immintrin.h>
#include <omp.h>
#include <iostream>
using namespace std;
typedef unsigned long long uint64;
double test_dp_mac_AVX(double x,double y,uint64 iterations){
register __m256d r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,rA,rB,rC,rD,rE,rF;
// Generate starting data.
r0 = _mm256_set1_pd(x);
r1 = _mm256_set1_pd(y);
r8 = _mm256_set1_pd(-0.0);
r2 = _mm256_xor_pd(r0,r8);
r3 = _mm256_or_pd(r0,r8);
r4 = _mm256_andnot_pd(r8,r0);
r5 = _mm256_mul_pd(r1,_mm256_set1_pd(0.37796447300922722721));
r6 = _mm256_mul_pd(r1,_mm256_set1_pd(0.24253562503633297352));
r7 = _mm256_mul_pd(r1,_mm256_set1_pd(4.1231056256176605498));
r8 = _mm256_add_pd(r0,_mm256_set1_pd(0.37796447300922722721));
r9 = _mm256_add_pd(r1,_mm256_set1_pd(0.24253562503633297352));
rA = _mm256_sub_pd(r0,_mm256_set1_pd(4.1231056256176605498));
rB = _mm256_sub_pd(r1,_mm256_set1_pd(4.1231056256176605498));
rC = _mm256_set1_pd(1.4142135623730950488);
rD = _mm256_set1_pd(1.7320508075688772935);
rE = _mm256_set1_pd(0.57735026918962576451);
rF = _mm256_set1_pd(0.70710678118654752440);
uint64 iMASK = 0x800fffffffffffffull;
__m256d MASK = _mm256_set1_pd(*(double*)&iMASK);
__m256d vONE = _mm256_set1_pd(1.0);
uint64 c = 0;
while (c < iterations){
size_t i = 0;
while (i < 1000){
// Here's the meat - the part that really matters.
r0 = _mm256_mul_pd(r0,rC);
r1 = _mm256_add_pd(r1,rD);
r2 = _mm256_mul_pd(r2,rE);
r3 = _mm256_sub_pd(r3,rF);
r4 = _mm256_mul_pd(r4,rC);
r5 = _mm256_add_pd(r5,rD);
r6 = _mm256_mul_pd(r6,rE);
r7 = _mm256_sub_pd(r7,rF);
r8 = _mm256_mul_pd(r8,rC);
r9 = _mm256_add_pd(r9,rD);
rA = _mm256_mul_pd(rA,rE);
rB = _mm256_sub_pd(rB,rF);
r0 = _mm256_add_pd(r0,rF);
r1 = _mm256_mul_pd(r1,rE);
r2 = _mm256_sub_pd(r2,rD);
r3 = _mm256_mul_pd(r3,rC);
r4 = _mm256_add_pd(r4,rF);
r5 = _mm256_mul_pd(r5,rE);
r6 = _mm256_sub_pd(r6,rD);
r7 = _mm256_mul_pd(r7,rC);
r8 = _mm256_add_pd(r8,rF);
r9 = _mm256_mul_pd(r9,rE);
rA = _mm256_sub_pd(rA,rD);
rB = _mm256_mul_pd(rB,rC);
r0 = _mm256_mul_pd(r0,rC);
r1 = _mm256_add_pd(r1,rD);
r2 = _mm256_mul_pd(r2,rE);
r3 = _mm256_sub_pd(r3,rF);
r4 = _mm256_mul_pd(r4,rC);
r5 = _mm256_add_pd(r5,rD);
r6 = _mm256_mul_pd(r6,rE);
r7 = _mm256_sub_pd(r7,rF);
r8 = _mm256_mul_pd(r8,rC);
r9 = _mm256_add_pd(r9,rD);
rA = _mm256_mul_pd(rA,rE);
rB = _mm256_sub_pd(rB,rF);
r0 = _mm256_add_pd(r0,rF);
r1 = _mm256_mul_pd(r1,rE);
r2 = _mm256_sub_pd(r2,rD);
r3 = _mm256_mul_pd(r3,rC);
r4 = _mm256_add_pd(r4,rF);
r5 = _mm256_mul_pd(r5,rE);
r6 = _mm256_sub_pd(r6,rD);
r7 = _mm256_mul_pd(r7,rC);
r8 = _mm256_add_pd(r8,rF);
r9 = _mm256_mul_pd(r9,rE);
rA = _mm256_sub_pd(rA,rD);
rB = _mm256_mul_pd(rB,rC);
i++;
}
// Need to renormalize to prevent denormal/overflow.
r0 = _mm256_and_pd(r0,MASK);
r1 = _mm256_and_pd(r1,MASK);
r2 = _mm256_and_pd(r2,MASK);
r3 = _mm256_and_pd(r3,MASK);
r4 = _mm256_and_pd(r4,MASK);
r5 = _mm256_and_pd(r5,MASK);
r6 = _mm256_and_pd(r6,MASK);
r7 = _mm256_and_pd(r7,MASK);
r8 = _mm256_and_pd(r8,MASK);
r9 = _mm256_and_pd(r9,MASK);
rA = _mm256_and_pd(rA,MASK);
rB = _mm256_and_pd(rB,MASK);
r0 = _mm256_or_pd(r0,vONE);
r1 = _mm256_or_pd(r1,vONE);
r2 = _mm256_or_pd(r2,vONE);
r3 = _mm256_or_pd(r3,vONE);
r4 = _mm256_or_pd(r4,vONE);
r5 = _mm256_or_pd(r5,vONE);
r6 = _mm256_or_pd(r6,vONE);
r7 = _mm256_or_pd(r7,vONE);
r8 = _mm256_or_pd(r8,vONE);
r9 = _mm256_or_pd(r9,vONE);
rA = _mm256_or_pd(rA,vONE);
rB = _mm256_or_pd(rB,vONE);
c++;
}
r0 = _mm256_add_pd(r0,r1);
r2 = _mm256_add_pd(r2,r3);
r4 = _mm256_add_pd(r4,r5);
r6 = _mm256_add_pd(r6,r7);
r8 = _mm256_add_pd(r8,r9);
rA = _mm256_add_pd(rA,rB);
r0 = _mm256_add_pd(r0,r2);
r4 = _mm256_add_pd(r4,r6);
r8 = _mm256_add_pd(r8,rA);
r0 = _mm256_add_pd(r0,r4);
r0 = _mm256_add_pd(r0,r8);
// Prevent Dead Code Elimination
double out = 0;
__m256d temp = r0;
out += ((double*)&temp)[0];
out += ((double*)&temp)[1];
out += ((double*)&temp)[2];
out += ((double*)&temp)[3];
return out;
}
void test_dp_mac_AVX(int tds,uint64 iterations){
double *sum = (double*)malloc(tds * sizeof(double));
double start = omp_get_wtime();
#pragma omp parallel num_threads(tds)
{
double ret = test_dp_mac_AVX(1.1,2.1,iterations);
sum[omp_get_thread_num()] = ret;
}
double secs = omp_get_wtime() - start;
uint64 ops = 48 * 1000 * iterations * tds * 4;
cout << "Seconds = " << secs << endl;
cout << "FP Ops = " << ops << endl;
cout << "FLOPs = " << ops / secs << endl;
double out = 0;
int c = 0;
while (c < tds){
out += sum[c++];
}
cout << "sum = " << out << endl;
cout << endl;
free(sum);
}
int main(){
// (threads, iterations)
test_dp_mac_AVX(8,10000000);
system("pause");
}
Sortie (1 thread, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 - Version x64:
Seconds = 57.4679
FP Ops = 1920000000000
FLOPs = 3.34099e+010
sum = 4.45305
Le pic théorique de l’AVX est de 8 flops * 4,4 GHz = 35,2 GFlops . La valeur réelle est égale à 33,4 GFlops .
Sortie (8 threads, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 - Version x64:
Seconds = 111.119
FP Ops = 15360000000000
FLOPs = 1.3823e+011
sum = 35.6244
Le pic théorique de l’AVX est de 8 flops * 4 cœurs * 4,4 GHz = 140,8 GFlops. La valeur réelle est 138,2 GFlops .
Maintenant pour quelques explications:
La partie critique de la performance est évidemment les 48 instructions à l'intérieur de la boucle interne. Vous remarquerez qu'il est divisé en 4 blocs de 12 instructions chacun. Chacun de ces 12 blocs d'instructions est complètement indépendant l'un de l'autre et nécessite en moyenne 6 cycles d'exécution.
Il y a donc 12 instructions et 6 cycles entre les problèmes à utiliser. La latence de la multiplication est de 5 cycles, il suffit donc d'éviter les blocages de latence.
L'étape de normalisation est nécessaire pour empêcher les données de déborder ou de déborder. Cela est nécessaire car le code "ne rien faire" augmentera/diminuera lentement la magnitude des données.
Il est donc possible de faire mieux que cela si vous utilisez uniquement des zéros et que vous vous débarrassez de l'étape de normalisation. Cependant, depuis que j'ai écrit le repère pour mesurer la consommation d'énergie et la température, , je devais m'assurer que les flops étaient sur des données "réelles" plutôt que sur des zéros - comme les unités d’exécution peuvent très bien avoir un traitement spécial des cas pour les zéros qui utilisent moins d’énergie et produisent moins de chaleur.
Discussions: 1
Seconds = 72.1116
FP Ops = 960000000000
FLOPs = 1.33127e+010
sum = 2.22652
Théorie SSE Crête: 4 flops * 3,5 GHz = 14,0 GFlops . La valeur réelle est égale à 13,3 GFlops .
Discussions: 8
Seconds = 149.576
FP Ops = 7680000000000
FLOPs = 5.13452e+010
sum = 17.8122
Théorie SSE Pic: 4 flops * 4 cœurs * 3,5 GHz = 56,0 GFlops . La valeur réelle est égale à 51,3 GFlops .
Le temps de traitement de mon processeur a atteint 76 ° C lors de l'exécution multithread! Si vous les exécutez, assurez-vous que les résultats ne sont pas affectés par la limitation du processeur.
Discussions: 1
Seconds = 78.3357
FP Ops = 960000000000
FLOPs = 1.22549e+10
sum = 2.22652
Théorie SSE Crête: 4 bascules * 3,2 GHz = 12,8 GFlops . La valeur réelle est 12,3 GFlops .
Discussions: 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
Théorie SSE Pic: 4 flops * 8 cœurs * 3,2 GHz = 102,4 GFlops . La valeur actuelle est égale à 97,9 GFlops .
Il y a un point dans l'architecture Intel que les gens oublient souvent, les ports de répartition sont partagés entre Int et FP/SIMD. Cela signifie que vous n'obtiendrez qu'une certaine quantité de rafales de FP/SIMD avant que la logique de boucle ne crée des bulles dans votre flux en virgule flottante. Mystical a eu plus de flops dans son code, car il utilisait des pas plus longs dans sa boucle non déroulée.
Si vous regardez l'architecture Nehalem/Sandy Bridge ici http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 c'est très clair ce qui se passe.
En revanche, il devrait être plus facile d’atteindre des performances optimales sur AMD (Bulldozer), car les canaux INT et FP/SIMD ont des ports d’émission distincts avec leur propre planificateur.
Ce n'est que théorique car je n'ai aucun de ces processeurs à tester.
Les branches peuvent certainement vous empêcher de maintenir des performances théoriques optimales. Voyez-vous une différence si vous effectuez manuellement un déroulement en boucle? Par exemple, si vous mettez 5 ou 10 fois plus d'opérations par itération de boucle:
for(int i=0; i<loops/5; i++) {
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
}
Avec Intels icc version 11.1 sur un processeur Intel Core 2 Duo à 2,4 GHz, je reçois
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000
addmul: 0.105 s, 9.525 Gflops, res=0.000000
Macintosh:~ mackie$ icc -v
Version 11.1
C'est très proche de l'idéal 9,6 Gflops.
MODIFIER:
Oops, en regardant le code de l'Assemblée, il semble que la CIC n'ait pas seulement vectorisé la multiplication, mais ait également retiré les ajouts de la boucle. Forcant une sémantique fp plus stricte, le code n'est plus vectorisé:
Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul: 0.516 s, 1.938 Gflops, res=1.326463
EDIT2:
Comme demandé:
Macintosh:~ mackie$ clang -O3 -mssse3 -oaddmul addmul.cc && ./addmul 1000
addmul: 0.209 s, 4.786 Gflops, res=1.326463
Macintosh:~ mackie$ clang -v
Apple clang version 3.0 (tags/Apple/clang-211.10.1) (based on LLVM 3.0svn)
Target: x86_64-Apple-darwin11.2.0
Thread model: posix
La boucle interne du code de clang ressemble à ceci:
.align 4, 0x90
LBB2_4: ## =>This Inner Loop Header: Depth=1
addsd %xmm2, %xmm3
addsd %xmm2, %xmm14
addsd %xmm2, %xmm5
addsd %xmm2, %xmm1
addsd %xmm2, %xmm4
mulsd %xmm2, %xmm0
mulsd %xmm2, %xmm6
mulsd %xmm2, %xmm7
mulsd %xmm2, %xmm11
mulsd %xmm2, %xmm13
incl %eax
cmpl %r14d, %eax
jl LBB2_4
EDIT3:
Enfin, deux suggestions: Tout d’abord, si vous aimez ce type d’analyse comparative, envisagez d’utiliser l’instruction rdtsc
au lieu de gettimeofday(2)
. Il est beaucoup plus précis et délivre le temps en cycles, ce qui vous intéresse de toute façon. Pour gcc et ses amis, vous pouvez le définir comme ceci:
#include <stdint.h>
static __inline__ uint64_t rdtsc(void)
{
uint64_t rval;
__asm__ volatile ("rdtsc" : "=A" (rval));
return rval;
}
Deuxièmement, vous devez exécuter votre programme de test plusieurs fois et utiliser la valeur meilleure performance uniquement. Dans les systèmes d'exploitation modernes, beaucoup de choses se passent en parallèle, le processeur peut être en mode d'économie d'énergie basse fréquence, etc. L'exécution répétée du programme vous donne un résultat plus proche du cas idéal.