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

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

autant que je sache, il faut trois cycles pour un SSE add et cinq cycles pour un mul à compléter sur la plupart des processeurs Intel modernes (voir par exemple Agner Fog's 'Instruction Tables' ). Grâce à pipelining on peut obtenir un débit de un add par cycle si l'algorithme a au moins trois sommations. Comme cela est vrai pour addpd emballé ainsi que le scalar addsd versions et les registres SSE peuvent contenir deux double 's le débit peut être autant que deux flops par cycle.

de plus, il semble (bien que je n'ai pas vu de documentation appropriée à ce sujet) add 's et mul 's peuvent être exécutés en parallèle donnant un débit théorique max 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 s'est soldée par environ 2,7 dérapages/cycle. Si quelqu'un peut contribuer à un simple programme de C/C++ ou d'assembleur qui montre des performances de pointe qui seraient grandement appréciées.

mon essai:

#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 une 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. En regardant le code assembleur avec g++ -S -O2 -march=native -masm=intel addmul.cpp la boucle principale semble sorte 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 les versions scalaires avec les versions emballées ( addpd et mulpd ) doublerait le nombre de flop sans changer le temps d'exécution et donc je serais à court de 2,8 flops par cycle. Y a - t-il un exemple simple qui réalise quatre flops par cycle?

Nice little programme by Mysticial; voici mes résultats (exécuter juste pour quelques secondes cependant):

  • gcc -O2 -march=nocona : 5.6 Gflops out of 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 des changements de l'ordre des indépendants d'opérations en virgule flottante avec le but de l'alternance addpd et mulpd si possible. Il en va de même pour gcc-4.6.2 -O2 -march=core2 .

  • gcc -O2 -march=nocona semble maintenir l'ordre des opérations à virgule flottante tel que défini dans la source C++.

  • cl /O2 , le compilateur 64 bits de la SDK pour Windows 7 fait boucler-dérouler automatiquement et semble essayer d'organiser des opérations de sorte que les groupes de trois addpd 's alternent avec trois mulpd 's (bien, au moins sur mon système et pour mon programme simple).

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

  • D'autres architectures (peut-être Sandy Bridge et autres) semblent être capable d'exécuter add/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 à faible niveau d'optimisation des opérations pour mon système et atteint des performances proches de la pointe pour le petit exemple de C++ ci-dessus. J'ai mesuré entre 1.85-2.01 flops / cycle (ont utilisé horloge () dans les fenêtres qui n'est pas si précis. Je suppose que, plus besoin d'utiliser une meilleure minuterie - grâce Mackie Messer).

  • le mieux que j'ai géré avec gcc était de boucler manuellement déroulez et arranger additions et multiplications en 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 qui me satisfait maintenant.

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
...
554
demandé sur Peter Mortensen 2011-12-05 21:54:56
la source

4 ответов

j'ai déjà fait cette tâche. Mais il s'agissait principalement de mesurer la consommation électrique et les températures CPU. Le code suivant (qui est assez long) atteint presque optimal sur mon cœur i7 2600K.

la chose clé à noter ici est la quantité massive de boucle manuelle déroulante ainsi que l'entrelacement des multiples et des additions...

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

avertissement:

si vous décidez de compiler et d'exécuter ceci, faites attention à vos températures CPU!!!

assurez-vous de ne pas la surchauffer. Et assurez-vous que L'étranglement CPU n'affecte pas vos résultats!

de plus, Je ne prends aucune responsabilité pour les dommages qui pourraient résulter de l'utilisation 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 bien fonctionner sur Visual Studio 2010/2012 et GCC 4.6.

    ICC 11 (compilateur Intel 11) a étonnamment de la difficulté à le compiler correctement.
  • ceux-ci sont pour les processeurs pré-FMA. Afin de réaliser des pics sur les processeurs Intel Haswell et AMD Bulldozer (et plus tard), FMA (Fusionné Multiplier Ajouter) des instructions seront nécessaires. Celles-ci dépassent le cadre du présent 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");
}

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 noyau i7 2600K @ 4.4 GHz. Le pic théorique de L'ESS 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-x64 Version:

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

le pic théorique de L'ESS est de 4 flops * 4 core * 4,4 GHz = 70,4 GFlops. Réel 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-x64 Version:

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

theoretical AVX peak is 8 flops * 4.4 GHz = 35.2 GFlops . L'actuel est 33.4 GFlops .

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

le pic théorique AVX est 8 flops * 4 core * 4.4 GHz = 140.8 GFlops. Réel 138.2 GFlops .


maintenant, quelques explications:

la pièce de performance critique est évidemment les 48 instructions à l'intérieur de la boucle intérieure. 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 délivrance et l'utilisation. La latence de multiplication est de 5 cycles, donc c'est juste assez pour éviter les décrochages 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 l'absence de code va augmenter/diminuer l'ampleur des données.

donc c'est en fait possible de faire mieux que cela si vous utilisez juste tous les zéros et de se 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, j'ai dû m'assurer que les flops étaient sur des données" réelles", plutôt que des zéros - comme les unités d'exécution peuvent très bien avoir cas spécial-manipulation pour les zéros qui utilisent moins de puissance et de produire moins de chaleur.


Plus De Résultats:

  • Intel Core i7 920 @ 3.5 GHz
  • Windows 7 Ultimate x64
  • Visual Studio 2010 SP1-x64 Release

Filetage: 1

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

theoretical SSE Peak: 4 flops * 3.5 GHz = 14.0 GFlops . Le nombre réel est 13.3 GFlops .

Threads: 8

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

theoretical SSE Peak: 4 flops * 4 Core * 3.5 GHz = 56.0 GFlops . Le numéro actuel est 51.3 GFlops .

mon processeur temps hit 76C sur le multi-threaded run! Si vous exécutez ceux-ci, assurez-vous que les résultats ne sont pas affectés par l'étranglement CPU.


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

Filetage: 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 = De 12,8 GFlops . L'actuel est 12.3 GFlops .

Fils: 8

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

Pic théorique SSE: 4 flops * 8 Core * 3.2 GHz = 102.4 GFlops . L'actuel est 97,9 GFlops .

455
répondu Mysticial 2013-08-16 20:46:41
la source

il y a un point dans L'architecture Intel que les gens oublient souvent, les ports d'expédition 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 flottant. Mystical a obtenu plus de flops hors de son code, parce qu'il a utilisé de plus longues enjambées dans sa boucle déroulée.

si vous regardez l'architecture du Pont Nehalem/Sandy 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 d'atteindre la performance de pointe sur AMD (Bulldozer) que les tuyaux INT et FP/SIMD ont des ports d'émission séparés avec leur propre scheduler.

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

29
répondu Patrick Schlüter 2011-12-06 20:05:20
la source

Branches peuvent certainement vous empêcher de soutenir des performances théoriques de pointe. Voyez-vous une différence si vous faites manuellement une boucle-déroulante? Par exemple, si vous mettez 5 ou 10 fois plus d'ops 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;
   }
15
répondu TJD 2011-12-05 22:04:38
la source

en utilisant la version icc d'Intel11. 1 sur un Duo Intel Core 2 2.4 GHz I get

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

EDIT:

Oops, en regardant le code d'assemblage, il semble que la CCI non seulement vectorisé la multiplication, mais aussi tiré les ajouts hors 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 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 de benchmarking, 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 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 référence plusieurs fois et utiliser le meilleure performance seulement . Dans les systèmes d'exploitation modernes beaucoup de choses se produisent en parallèle, le cpu peut être dans un mode d'économie d'énergie de basse fréquence, etc. L'exécution du programme à plusieurs reprises vous donne un résultat qui est plus proche du cas idéal.

6
répondu Mackie Messer 2011-12-06 19:36:53
la source

Autres questions sur c++ c optimization assembly architecture