Question Comment atteindre le maximum théorique de 4 FLOP par cycle?


Comment atteindre la performance maximale théorique de 4 opérations en virgule flottante (double précision) par cycle sur un processeur Intel x86-64 moderne?

Autant que je comprends, prendre trois cycles pour un SSE  add et cinq cycles pour un mul pour compléter sur la plupart des processeurs Intel modernes (voir par exemple Les «tableaux d'instructions» d'Agner Fog ). En raison de pipelining, on peut obtenir un débit d'un add par cycle si l'algorithme a au moins trois sommations indépendantes. Puisque c'est vrai pour emballé addpd ainsi que le scalaire addsd versions et les registres SSE peuvent contenir deux doubleLe débit peut atteindre deux flops par cycle.

En outre, il semble (bien que je n'ai pas vu de documentation appropriée à ce sujet) add'le sable mulpeut être exécuté en parallèle en donnant un débit théorique maximum de quatre flops par cycle.

Cependant, je n'ai pas été capable de répliquer cette performance 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 qui démontre une performance maximale qui serait grandement appréciée.

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 flops par cycle. Regarder le code de l'assembleur avec g++ -S -O2 -march=native -masm=intel addmul.cpp la boucle principale semble un peu optimal pour moi:

.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 et donc j'aurais juste un peu moins de 2,8 flops par cycle. Y at-il un exemple simple qui réalise quatre flops par cycle?

Beau petit programme de Mysticial; voici mes résultats (juste pour 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 semble un peu complexe, mais mes conclusions jusqu'à présent:

  • gcc -O2 modifie l'ordre des opérations de virgule flottante indépendantes avec le but de l'alternance addpd et mulpdest si possible. La même chose s'applique à gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona semble garder l'ordre des opérations en virgule flottante comme défini dans la source C ++.

  • cl /O2, le compilateur 64 bits du SDK pour Windows 7 boucle-dérouler automatiquement et semble essayer et arranger les opérations de sorte que les groupes de trois addpdest en alternance avec trois mulpdde (enfin, au moins sur mon système et pour mon programme simple).

  • ma Core i5 750 (Nahelem architecture) n'aime pas alterner add et mul's et semble incapable pour exécuter les deux opérations en parallèle. Cependant, s'il est groupé en 3, il fonctionne soudainement comme de la magie.

  • Autres architectures (éventuellement Sandy Bridge et d'autres) semblent être capable d'exécuter add / mul en parallèle sans problèmes s'ils alternent dans le code d'assemblage.

  • Bien que difficile à admettre, mais sur mon système cl /O2 fait un bien meilleur travail à des opérations d'optimisation de bas niveau pour mon système et atteint des performances proches de la pointe 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 si précis, j'imagine, il faut utiliser une meilleure minuterie - merci Mackie Messer).

  • Le meilleur que j'ai réussi avec gcc était de boucler manuellement dérouler et organiser additions et multiplications par groupes de trois. Avec g++ -O2 -march=nocona addmul_unroll.cpp Je reçois au mieux 0.207s, 4.825 Gflops ce qui correspond à 1,8 flops / cycle ce dont je suis assez content maintenant.

Dans le code C ++ j'ai remplacé le for boucle avec

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

545
2017-12-05 17:54


origine


Réponses:


J'ai déjà fait cette tâche exacte. Mais c'était surtout pour mesurer la consommation d'énergie et les températures du CPU. Le code suivant (qui est assez long) se rapproche de mon Core i7 2600K.

La principale chose à noter ici est la quantité massive de déroulement manuel de la boucle ainsi que l'entrelacement des multiplications et des ajouts ...

Le projet complet peut être trouvé sur mon GitHub: https://github.com/Mysticial/Flops

Attention:

Si vous décidez de compiler et d'exécuter ceci, faites attention aux températures de votre CPU !!!
Assurez-vous de ne pas le surchauffer. Et assurez-vous que la limitation du processeur n'affecte pas vos résultats!

De plus, je ne suis pas responsable des dommages qui pourraient résulter de l'exécution de ce code.

Remarques:

  • 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 sur Visual Studio 2010/2012 et GCC 4.6.
    ICC 11 (Intel Compiler 11) a étonnamment du mal à bien le compiler.
  • Ce sont pour les processeurs pré-FMA. Afin d'obtenir des FLOPS de pointe sur les processeurs Intel Haswell et AMD Bulldozer (et plus tard), des instructions FMA (Fused Multiply Add) seront nécessaires. Ceux-ci sont au-delà de la portée de ce repère.

#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. Le pic SSE théorique 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

Le pic SSE théorique est de 4 flops * 4 cœurs * 4,4 GHz = 70,4 GFlops. Actuel est 65.5 GFlops.


Prenons 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 AVX théorique est de 8 flops * 4,4 GHz = 35.2 GFlops. Actuel est 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 AVX théorique est de 8 flops * 4 cœurs * 4,4 GHz = 140.8 GFlops. Actuel est 138.2 GFlops.


Maintenant pour quelques explications:

La partie critique de 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 prend en moyenne 6 cycles à exécuter.

Il y a donc 12 instructions et 6 cycles entre la mise en service. La latence de la multiplication est de 5 cycles, donc c'est juste assez pour éviter les arrêts de latence.

L'étape de normalisation est nécessaire pour éviter que les données ne dépassent ou ne débordent. Ceci est nécessaire puisque le code do-nothing augmentera / diminuera lentement l'amplitude des données.

Donc, il est effectivement possible de faire mieux que cela si vous utilisez simplement tous les zéros et vous débarrasser de l'étape de normalisation. Cependant, depuis que j'ai écrit le benchmark pour mesurer la consommation d'énergie et la température, Je devais m'assurer que les flops étaient sur de "vraies" données plutôt que sur des zéros - car les unités d'exécution peuvent très bien avoir une gestion de cas spéciale pour les zéros qui consomment moins d'énergie et produisent moins de chaleur.


Plus de résultats:

  • Intel Core i7 920 à 3,5 GHz
  • Windows 7 Édition Intégrale x64
  • Version Visual Studio 2010 SP1 - x64

Sujets: 1

Seconds = 72.1116
FP Ops  = 960000000000
FLOPs   = 1.33127e+010
sum = 2.22652

Pic SSE théorique: 4 flops * 3.5 GHz = 14.0 GFlops. Actuel est 13.3 GFlops.

Discussions: 8

Seconds = 149.576
FP Ops  = 7680000000000
FLOPs   = 5.13452e+010
sum = 17.8122

Pic SSE théorique: 4 flops * 4 coeurs * 3.5 GHz = 56.0 GFlops. Actuel est 51.3 GFlops.

Mon processeur temps a atteint 76C sur la course multi-thread! Si vous les exécutez, assurez-vous que les résultats ne sont pas affectés par la limitation du processeur.


444
2017-12-05 20:43



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'un certain nombre de salves de FP / SIMD avant que la logique de boucle ne crée des bulles dans votre flux à virgule flottante. Mystical a retiré plus de flops de son code, parce qu'il utilisait des foulées plus longues dans sa boucle 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 tubes INT et FP / SIMD ont des ports de sortie distincts avec leur propre planificateur.

Ce n'est que théorique car je n'ai aucun de ces processeurs à tester.


29
2017-12-06 16:05



Les succursales peuvent certainement vous empêcher de maintenir la performance théorique maximale. Voyez-vous une différence si vous faites manuellement un bouclage de boucle? Par exemple, si vous mettez 5 ou 10 fois plus d'opérations par boucle d'itération:

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

15
2017-12-05 18:04



En utilisant Intels icc Version 11.1 sur un processeur Intel Core 2 Duo 2,4 GHz

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:

Oups, en regardant le code de l'assemblage, il semble que icc a non seulement vectorisé la multiplication, mais aussi retiré les ajouts de la boucle. Forcer 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: d'abord, si vous aimez ce type de benchmarking, pensez à utiliser le rdtsc instruction est de gettimeofday(2). Il est beaucoup plus précis et fournit le temps en cycles, ce qui est généralement ce qui vous intéresse de toute façon. Pour gcc et 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 devriez exécuter votre programme de référence plusieurs fois et utiliser le meilleure performance seulement. Dans les systèmes d'exploitation modernes, beaucoup de choses se passent en parallèle, l'unité centrale 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.


6
2017-12-05 20:19