698 votes

Comment pour atteindre le maximum théorique de 4 opérations en virgule flottante par cycle ?

Comment peut théoriques de la performance de pointe de 4 opérations en virgule flottante (double précision) par cycle réalisée sur une moderne x86-64 d'Intel cpu?

Comme je le comprends il prendre 3 cycles pour une ess add et 5 cycles pour un mul sur la plupart des modernes Intel cpu (voir, par exemple, Agner de la Brume 'Instruction des Tables ). En raison de pipelining, on peut obtenir un débit de 1 add par cycle si l'algorithme a au moins 3 indépendant débats. Puisque c'est vrai pour les paniers addpd ainsi que le scalaire addsd versions et des registres sse peuvent contenir de 2 double's le débit peut être autant que 2 flops par cycle. En outre, il semble (bien que je n'ai pas vu un doc sur ce sujet) adds et muls'peuvent être exécutées en parallèle, donnant un débit maximal théorique de 4 flops par cycle.

Cependant, je n'ai pas été en mesure de reproduire cette performance avec un simple c/c++ programme. Ma meilleure tentative a abouti à environ 2,7 flops/cycle. Si n'importe qui peut contribuer à un simple c/c++ / assembleur programme qui démontre des performances de pointe, ce serait grandement apprécié.

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 ops 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 juste au sujet de 1.4 flops par cycle. En regardant le code assembleur avec g++ -S -O2 -march=native -masm=intel addmul.cpp la boucle principale semble genre de 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 le scalaire des versions avec des paniers versions (addpd et mulpd) permettrait de doubler le flop comte sans changer le temps d'exécution et donc j'aurais juste un peu moins de 2,8 flops par cycle. Un simple exemple qui réalise 4 flops par cycle?

Edit:

Joli petit programme par Mysticial, voici mes résultats (course juste pour quelques secondes tout de même):

  • gcc -O2 -march=nocona: 5.6 Gflops de 10.66 Gflops (2.1 flops/cycle)
  • cl /O2, openmp supprimée: 10.1 Gflops de 10.66 Gflops (3.8 flops/cycle)

Tout cela semble un peu complexe, mais mes conclusions à ce jour:

  • gcc -O2 de modifier l'ordre des indépendants d'opérations en virgule flottante avec le but de l'alternance addpd et mulpds', si possible. Même s'applique à l' gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona semble garder la commande de la pf opérations tel que défini dans la source C++.

  • cl /O2, le compilateur 64 bits à partir de la SDK pour Windows 7 ne boucle de dérouler automatiquement et semble essayer d'organiser des opérations de de sorte que les groupes de 3 addpds'alternent avec 3 mulpd'(au moins sur mon système et pour mon simple programme).

  • Mon Core i5 750 (Nahelem architecture) n'aime pas l'alternance add et mul et ne semble pas en mesure pour exécuter à la fois de la fpo en parallèle. Toutefois, si regroupées en 3 soudain, elle fonctionne comme de la magie.

  • D'autres architectures (éventuellement Sandy Bridge et d'autres) semblent être en mesure d'exécuter ajouter/mul en parallèle sans problèmes si ils alternent dans le code assembleur.

  • Bien que difficile à admettre, mais, sur mon système, cl /O2 fait un bien meilleur travail au niveau de l'optimisation de l'exploitation pour mon système, et réalise près de pointe performance pour le petit c++ exemple ci-dessus. J'ai mesuré entre 1.85-2.01 flops/cycle (ont utilisé de l'horloge() dans Windows qui n'est pas très précis Je suppose que, plus besoin d'utiliser une meilleure minuterie - grâce Mackie Messer).

  • Le meilleur que j'ai réussi avec gcc a été manuellement boucle dérouler et organiser additions et de multiplications en groupes de trois. Avec g++ -O2 -march=nocona addmul_unroll.cpp J'obtiens au mieux, 0.207s, 4.825 Gflops ce qui correspond à 1,8 flops/cycle je suis très heureux maintenant.

Dans le code c++ j'ai remplacé l' 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
...

562voto

Mysticial Points 180300

J'ai fait cette tâche avant. Mais c'était principalement pour mesurer la consommation d'énergie et les températures CPU. Le code suivant (qui est assez long) atteint presque optimale sur mon Core i7 2600K.

Le point clé ici est de noter la quantité massive de manuel boucle-déroulage ainsi que l'entrelacement de multiplie les et ajoute...

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

Avertissement:

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

En outre, je ne prends aucune responsabilité pour quelque dommage qui pourrait 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 bien sur Visual Studio 2010/2012 et GCC 4.6.
    CPI 11 (Intel Compilateur 11), étonnamment, a de la difficulté à le compiler.
  • Ce sont pour la pré-FMA processeurs. Afin d'atteindre des pics de FLOPS sur les processeurs Intel Haswell et les processeurs AMD Bulldozer (et plus tard), FMA (Fused Multiply Ajouter) des instructions seront nécessaires. Ces sont au-delà de la portée de cet indice de 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");
}

De sortie (1 thread, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 x64 Version:

Seconds = 55.5104
FP Ops  = 960000000000
FLOPs   = 1.7294e+010
sum = 2.22652

La machine est un Core i7 2600K @ 4.4 GHz. Théorique de l'ESS de pointe est de 4 flops * 4.4 GHz = 17.6 GFlops. Ce code réalise 17.3 GFlops - pas mal.

De sortie (8 threads, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 x64 Version:

Seconds = 117.202
FP Ops  = 7680000000000
FLOPs   = 6.55279e+010
sum = 17.8122

Théorique de l'ESS de pointe est de 4 flops * 4 carottes * 4.4 GHz = 70.4 GFlops. Réel est de 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");
}

De sortie (1 thread, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 x64 Version:

Seconds = 57.4679
FP Ops  = 1920000000000
FLOPs   = 3.34099e+010
sum = 4.45305

Théorique AVX de pointe est de 8 flops * 4.4 GHz = 35.2 GFlops. Réel est de 33,4 GFlops.

De sortie (8 threads, 10000000 itérations) - Compilé avec Visual Studio 2010 SP1 x64 Version:

Seconds = 111.119
FP Ops  = 15360000000000
FLOPs   = 1.3823e+011
sum = 35.6244

Théorique AVX de pointe est de 8 flops * 4 carottes * 4.4 GHz = 140.8 GFlops. Réel est 138.2 GFlops.


Maintenant, pour quelques explications:

La critique pour les performances de la partie est bien évidemment l'48 instructions à l'intérieur de la boucle interne. Vous remarquerez qu'il est décomposé en 4 blocs de 12 instructions de chacun. Chacun de ces 12 instructions de blocs sont complètement indépendants les uns des autres, et, il faut en moyenne 6 cycles pour exécuter.

Donc, il y a 12 instructions et 6 cycles entre problème à utiliser. Le temps de latence de multiplication est de 5 cycles, il est donc juste assez pour éviter la latence des stands.

L'étape de normalisation est nécessaire de conserver les données de plus de/underflowing. Cela est nécessaire, car l'absence de code va augmenter/diminuer l'ampleur des données.

Il est donc effectivement possible de faire mieux que ça si vous venez d'utiliser tous les zéros et de se débarrasser de l'étape de normalisation. Cependant, depuis que j'ai écrit l'indice de référence pour mesurer la consommation électrique et la température, je devais m'assurer que les flops étaient en "réel" de données, plutôt que des zéros - comme les unités d'exécution peut très bien avoir des cas spéciaux de manutention 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 x64 Version

Fils: 1

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

Théorique de l'ESS de Pointe: 4 flops * 3.5 GHz = 14.0 GFlops. Réel est de 13,3 GFlops.

Threads: 8

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

Théorique de l'ESS de Pointe: 4 flops * 4 carottes * 3.5 GHz = 56.0 GFlops. Réel est de 51,3 GFlops.

Mon processeur temps de frapper 76C sur le multi-thread à exécuter! Si vous exécute ces, être sûr que les résultats ne sont donc pas concernées par la limitation des processeurs.


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

Fils: 1

Seconds = 78.3357
FP Ops  = 960000000000
FLOPs   = 1.22549e+10
sum = 2.22652

Théorique de l'ESS de Pointe: 4 flops * 3.2 GHz = 12.8 GFlops. Réel est de 12,3 GFlops.

Threads: 8

Seconds = 78.4733
FP Ops  = 7680000000000
FLOPs   = 9.78676e+10
sum = 17.8122

Théorique de l'ESS de Pointe: 4 flops * 8 carottes * 3.2 GHz = 102.4 GFlops. Réel est de 97,9 GFlops.

38voto

tristopia Points 5074

Il y a un point dans l'architecture Intel que les gens oublient souvent, l'envoi de ports sont partagés entre Int et FP/SIMD. Cela signifie que vous obtenez seulement une certaine quantité d'éclats de FP/SIMD avant la boucle logique de créer des bulles dans votre virgule flottante flux. Mystique a obtenu plus de flops de son code, car il a utilisé plus de progrès dans son déroulé de la boucle.

Si vous regardez le Nehalem/Sandy Bridge architecture ici http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 il est tout à fait clair ce qui se passe.

En revanche, il devrait être plus facile à atteindre des performances de pointe sur la DMLA (Bulldozer) que l'INT et FP/SIMD les tubes en question distincte des ports avec leur propre ordonnanceur.

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

16voto

TJD Points 7208

Branches peuvent certainement vous empêcher de soutenir les performances théoriques de pointe. Voyez-vous une différence si vous le faites manuellement certains boucle-dérouler ? Par exemple, si vous mettez des ops 5 ou 10 fois plus nombreux par itération de boucle :

8voto

Mackie Messer Points 3129

À l'aide de Intels cpi Version 11.1 sur un 2.4 GHz Intel Core 2 Duo-je obtenir

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 

Qui est très proche de l'idéal de 9,6 Gflops.

EDIT:

Oups, en regardant le code assembleur, il semble que la cci non seulement vectorisé la multiplication, mais aussi tiré les ajouts de la boucle. En forçant un plus strictes fp sémantique 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 intérieure de clang du code 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 de benchmarking, pensez à utiliser l' rdtsc instruction istead d' gettimeofday(2). Il est beaucoup plus précis et fournit le temps de 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 devez exécuter votre programme de test plusieurs fois et d'utiliser les meilleures performances. Dans les systèmes d'exploitation modernes beaucoup de choses se passent en parallèle, l'uc peut être dans une faible fréquence mode d'économie d'énergie, etc. L'exécution du programme à plusieurs reprises vous donne un résultat qui est plus proche du cas idéal.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X