web-dev-qa-db-fra.com

Comment puis-je atteindre le maximum théorique de 4 FLOP par cycle?

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
...
609
user1059432

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

Attention:

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:

  • Ce code est optimisé pour x64. x86 n'a pas assez de registres pour que cela compile bien.
  • Ce code a été testé pour fonctionner correctement avec Visual Studio 2010/2012 et GCC 4.6.
    ICC 11 (Intel Compiler 11) a étonnamment de la difficulté à bien le compiler.
  • Ce sont pour les processeurs pré-FMA. Pour atteindre le pic FLOPS sur les processeurs Intel Haswell et AMD Bulldozer (et ultérieurs), des instructions FMA (Fused Multiply Add) seront nécessaires. Ce sont au-delà de la portée de cette référence.
#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 .


Allons un peu plus loin. AVX ...

#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.


Plus de résultats:

  • Intel Core i7 920 à 3,5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1 - Version x64

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.


  • 2 x Intel Xeon X5482 Harpertown à 3,2 GHz
  • Ubuntu Linux 10 x64
  • GCC 4.5.2 x64 - (-O2 -msse3 -fopenmp)

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 .

496
Mysticial

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.

32
Patrick Schlüter

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;
   }
16
TJD

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.

7
Mackie Messer