Come posso ottenere il massimo teorico di 4 FLOP per ciclo?


642

Come è possibile ottenere le massime prestazioni teoriche di 4 operazioni in virgola mobile (doppia precisione) per ciclo su una moderna CPU Intel x86-64?

Per quanto ne so, sono necessari tre cicli per un SSE add e cinque cicli per un mulcompletamento sulla maggior parte delle moderne CPU Intel (vedere ad esempio le "Tabelle di istruzioni" di Agner Fog ). A causa del pipelining si può ottenere un throughput di uno addper ciclo se l'algoritmo ha almeno tre sommazioni indipendenti. Dal momento che questo è vero sia per addpdle addsdversioni impacchettate che per quelle scalari e i registri SSE possono contenere due double, il throughput può arrivare a due flop per ciclo.

Inoltre, sembra (anche se non ho visto alcuna documentazione adeguata su questo) add, mulè possibile eseguire in parallelo, dando un throughput massimo teorico di quattro flop per ciclo.

Tuttavia, non sono stato in grado di replicare quella prestazione con un semplice programma C / C ++. Il mio miglior tentativo ha portato a circa 2,7 flop / ciclo. Se qualcuno può contribuire con un semplice programma C / C ++ o assemblatore che dimostra le massime prestazioni che sarebbero molto apprezzate.

Il mio tentativo:

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

Compilato con

g++ -O2 -march=native addmul.cpp ; ./a.out 1000

produce il seguente output su un Intel Core i5-750, 2,66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Cioè, solo circa 1,4 flop per ciclo. Guardare il codice assembler con g++ -S -O2 -march=native -masm=intel addmul.cppil ciclo principale mi sembra un po 'ottimale:

.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

Cambiare le versioni scalari con versioni impacchettate ( addpde mulpd) raddoppierebbe il conteggio dei flop senza cambiare i tempi di esecuzione e quindi otterrei appena 2,8 flop per ciclo. C'è un semplice esempio che realizza quattro flop per ciclo?

Bel piccolo programma di Mysticial; ecco i miei risultati (esegui solo per pochi secondi):

  • gcc -O2 -march=nocona: 5.6 Gflop su 10.66 Gflop (2.1 flop / ciclo)
  • cl /O2, openmp rimosso: 10.1 Gflop su 10.66 Gflop (3.8 flop / ciclo)

Sembra tutto un po 'complesso, ma le mie conclusioni finora:

  • gcc -O2cambia l'ordine delle operazioni indipendenti in virgola mobile con l'obiettivo di alternare addpde mulpd'se possibile. Lo stesso vale per gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona sembra mantenere l'ordine delle operazioni in virgola mobile come definito nell'origine C ++.

  • cl /O2, il compilatore a 64 bit dell'SDK per Windows 7 esegue automaticamente lo srotolamento del ciclo e sembra tentare di organizzare le operazioni in modo che gruppi di tre si addpdalternino a tre mulpd(beh, almeno sul mio sistema e per il mio semplice programma) .

  • Il mio Core i5 750 ( architettura Nehalem ) non ama alternare add e mul e sembra incapace di eseguire entrambe le operazioni in parallelo. Tuttavia, se raggruppato in 3, all'improvviso funziona come per magia.

  • Altre architetture (possibilmente Sandy Bridge e altre) sembrano essere in grado di eseguire add / mul in parallelo senza problemi se si alternano nel codice assembly.

  • Anche se difficile da ammettere, ma sul mio sistema cl /O2fa un lavoro molto migliore con operazioni di ottimizzazione a basso livello per il mio sistema e raggiunge prestazioni quasi al massimo per il piccolo esempio C ++ sopra. Ho misurato tra 1,85-2,01 flop / ciclo (ho usato clock () in Windows che non è così preciso. Immagino, devo usare un timer migliore - grazie Mackie Messer).

  • Il migliore con cui sono riuscito è gccstato quello di srotolare manualmente e organizzare aggiunte e moltiplicazioni in gruppi di tre. Con g++ -O2 -march=nocona addmul_unroll.cpp ottengo nella migliore delle ipotesi 0.207s, 4.825 Gflopsche corrisponde a 1,8 flop / ciclo di cui ora sono abbastanza contento.

Nel codice C ++ ho sostituito il forciclo con

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

E l'assemblaggio ora sembra

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

15
Affidarsi al tempo di wallclock è probabilmente parte della causa. Supponendo che tu lo stia eseguendo all'interno di un sistema operativo come Linux, è libero di riprogrammare il tuo processo in qualsiasi momento. Questo tipo di evento esterno può influire sulle misurazioni delle prestazioni.
tdenniston,

Qual è la tua versione di GCC? Se utilizzi l'impostazione predefinita su un Mac, riscontrerai problemi (è un vecchio 4.2).
semisight

2
Sì, eseguendo Linux ma non c'è carico sul sistema e ripeterlo molte volte fa piccole differenze (ad esempio intervalli 4.0-4.2 Gflops per versione scalare, ma ora con -funroll-loops). Provato con gcc versione 4.4.1 e 4.6.2, ma l'output di ASM sembra ok?
user1059432

Hai provato -O3per gcc, che abilita -ftree-vectorize? Forse combinato con -funroll-loopssebbene non lo faccia se ciò è veramente necessario. Dopotutto il confronto sembra un po 'ingiusto se uno dei compilatori esegue la vettorializzazione / srotolamento, mentre l'altro non lo fa perché non può, ma perché non viene detto anche.
Grizzly,

4
@Grizzly -funroll-loopsè probabilmente qualcosa da provare. Ma penso che -ftree-vectorizesia oltre il punto. L'OP sta solo cercando di sostenere 1 mul + 1 aggiungere istruzioni / ciclo. Le istruzioni possono essere scalari o vettoriali - non importa poiché la latenza e la velocità effettiva sono le stesse. Quindi se puoi sostenere 2 / ciclo con SSE scalare, puoi sostituirli con SSE vettoriale e otterrai 4 flop / ciclo. Nella mia risposta ho fatto proprio questo andando da SSE -> AVX. Ho sostituito tutto l'SSE con AVX: stesse latenze, stessi throughput, 2x i flop.
Mistico il

Risposte:


517

Ho svolto esattamente questo compito prima. Ma era principalmente per misurare il consumo di energia e le temperature della CPU. Il seguente codice (che è abbastanza lungo) raggiunge quasi l'ottimale sul mio Core i7 2600K.

La cosa fondamentale da notare qui è l'enorme quantità di srotolamento manuale e l'interleaving di moltiplicatori e aggiunge ...

Il progetto completo è disponibile sul mio GitHub: https://github.com/Mysticial/Flops

Avvertimento:

Se decidi di compilare ed eseguire questo, presta attenzione alle temperature della tua CPU !!!
Assicurati di non surriscaldarlo. E assicurati che il throttling della CPU non influisca sui tuoi risultati!

Inoltre, non mi assumo alcuna responsabilità per qualsiasi danno che possa derivare dall'esecuzione di questo codice.

Appunti:

  • Questo codice è ottimizzato per x64. x86 non ha abbastanza registri per compilare bene questo.
  • Questo codice è stato testato per funzionare bene su Visual Studio 2010/2012 e GCC 4.6.
    ICC 11 (Intel Compiler 11) ha sorprendentemente problemi a compilarlo bene.
  • Questi sono per processori pre-FMA. Per ottenere i FLOPS di picco sui processori Intel Haswell e AMD Bulldozer (e successivi), saranno necessarie le istruzioni FMA (Fused Multiply Add). Questi vanno oltre lo scopo di questo benchmark.

#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");
}

Output (1 thread, 10000000 iterazioni) - Compilato con Visual Studio 2010 SP1 - Versione x64:

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

La macchina è un Core i7 2600 K a 4,4 GHz. Il picco teorico SSE è di 4 flop * 4,4 GHz = 17,6 GFlop . Questo codice ottiene 17,3 GFlop - non male.

Output (8 thread, 10000000 iterazioni) - Compilato con Visual Studio 2010 SP1 - Versione x64:

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

Il picco SSE teorico è di 4 flop * 4 core * 4,4 GHz = 70,4 GFlop. L'attuale è di 65,5 GFlop .


Facciamo un ulteriore passo avanti. 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");
}

Output (1 thread, 10000000 iterazioni) - Compilato con Visual Studio 2010 SP1 - Versione x64:

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

Il picco teorico AVX è di 8 flop * 4,4 GHz = 35,2 GFlop . L'attuale è di 33,4 GFlops .

Output (8 thread, 10000000 iterazioni) - Compilato con Visual Studio 2010 SP1 - Versione x64:

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

Il picco teorico AVX è di 8 flop * 4 core * 4,4 GHz = 140,8 GFlop. L'attuale è 138.2 GFlops .


Ora per alcune spiegazioni:

La parte critica delle prestazioni sono ovviamente le 48 istruzioni all'interno del circuito interno. Noterai che è suddiviso in 4 blocchi di 12 istruzioni ciascuno. Ognuno di questi 12 blocchi di istruzioni è completamente indipendente l'uno dall'altro e per eseguire in media 6 cicli.

Quindi ci sono 12 istruzioni e 6 cicli tra problema da usare. La latenza della moltiplicazione è di 5 cicli, quindi è sufficiente per evitare le stalle di latenza.

Il passaggio di normalizzazione è necessario per impedire che i dati si sovrappongano / si verifichino. Ciò è necessario poiché il codice Do-Nothing aumenterà / diminuirà lentamente l'entità dei dati.

Quindi in realtà è possibile fare di meglio se si utilizzano tutti gli zeri e si elimina il passaggio di normalizzazione. Tuttavia, dal momento che ho scritto il benchmark per misurare il consumo di energia e la temperatura, ho dovuto assicurarmi che i flop fossero su dati "reali", piuttosto che zeri - poiché le unità di esecuzione potrebbero benissimo avere una gestione del caso speciale per zeri che usano meno energia e produce meno calore.


Più risultati:

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

Discussioni: 1

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

Picco SSE teorico: 4 flop * 3,5 GHz = 14,0 GFlop . L'attuale è 13.3 GFlops .

Discussioni: 8

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

Picco SSE teorico: 4 flop * 4 core * 3,5 GHz = 56,0 GFlop . L'attuale è 51.3 GFlops .

Le temperature del mio processore hanno raggiunto 76C nella corsa multi-thread! Se li esegui, assicurati che i risultati non siano influenzati dalla limitazione della CPU.


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

Discussioni: 1

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

Picco SSE teorico: 4 flop * 3,2 GHz = 12,8 GFlop . L'attuale è 12.3 GFlops .

Discussioni: 8

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

Picco SSE teorico: 4 flop * 8 core * 3,2 GHz = 102,4 GFlop . L'attuale è 97,9 GFlops .


13
I tuoi risultati sono davvero impressionanti. Ho compilato il tuo codice con g ++ sul mio vecchio sistema, ma non ottengo risultati altrettanto positivi: 100.000 iterazioni, 1.814s, 5.292 Gflops, sum=0.448883su un picco di 10,68 Gflop o poco meno di 2,0 flop per ciclo. Sembra add/ mulnon vengono eseguiti in parallelo. Quando cambio il tuo codice e aggiungo / moltiplico sempre con lo stesso registro, diciamo rC, improvvisamente raggiunge quasi il picco: 0.953s, 10.068 Gflops, sum=0o 3,8 flop / ciclo. Molto strano.
user1059432

11
Sì, poiché non sto utilizzando l'assemblaggio in linea, le prestazioni sono davvero molto sensibili al compilatore. Il codice che ho qui è stato sintonizzato per VC2010. E se ricordo bene, il compilatore Intel offre altrettanto buoni risultati. Come hai notato, potrebbe essere necessario modificarlo un po 'per farlo compilare bene.
Mistico

8
Posso confermare i tuoi risultati su Windows 7 usando cl /O2(64-bit da Windows SDK) e anche il mio esempio funziona vicino al picco per operazioni scalari (1.9 flop / ciclo) lì. Il ciclo del compilatore si svolge e riordina, ma potrebbe non essere questo il motivo per cui è necessario approfondire ulteriormente questo aspetto. Limitare non è un problema Sono gentile con la mia CPU e mantengo iterazioni a 100k. :)
user1059432


2
@haylem Si scioglie o decolla. Mai entrambi. Se c'è abbastanza raffreddamento, otterrà il tempo di trasmissione. Altrimenti, si scioglie e basta. :)
Mistico il

33

C'è un punto nell'architettura Intel che la gente dimentica spesso, le porte di spedizione sono condivise tra Int e FP / SIMD. Ciò significa che otterrai solo una certa quantità di raffiche di FP / SIMD prima che la logica del loop crei bolle nel flusso a virgola mobile. Mystical ha ottenuto più flop dal suo codice, perché ha usato passi più lunghi nel suo ciclo srotolato.

Se guardi l'architettura Nehalem / Sandy Bridge qui http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 è abbastanza chiaro cosa succede.

Al contrario, dovrebbe essere più semplice raggiungere le massime prestazioni su AMD (Bulldozer) poiché le pipe INT e FP / SIMD hanno porte di emissione separate con il proprio scheduler.

Questo è solo teorico in quanto non ho nessuno di questi processori da testare.


2
Ci sono solo tre istruzioni del ciclo in testa: inc, cmp, e jl. Tutti questi possono andare alla porta # 5 e non interferire con il vettore faddo fmul. Preferirei sospettare che il decodificatore (a volte) si metta in mezzo. Deve sostenere da due a tre istruzioni per ciclo. Non ricordo i limiti esatti, ma la lunghezza delle istruzioni, i prefissi e l'allineamento entrano in gioco.
Mackie Messer,

cmpe jlcertamente vai alla porta 5, incnon così sicuro perché arriva sempre in gruppo con le altre 2. Ma hai ragione, è difficile dire dove sia il collo di bottiglia e anche i decodificatori possono farne parte.
Patrick Schlüter,

3
Ho giocato un po 'con il loop di base: l'ordinamento delle istruzioni è importante. Alcune disposizioni richiedono 13 cicli anziché i 5 cicli minimi. È ora di guardare i contatori degli eventi di spettacolo immagino ...
Mackie Messer il

16

Le filiali possono sicuramente impedirti di sostenere le massime prestazioni teoriche. Vedi una differenza se esegui manualmente un ciclo di srotolamento? Ad esempio, se inserisci 5 o 10 volte il numero di operazioni per iterazione loop:

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

4
Potrei sbagliarmi, ma credo che g ++ con -O2 tenterà di svolgere automaticamente il ciclo (penso che usi il dispositivo di Duff).
Weaver

6
Sì, grazie davvero migliora un po '. Ora ricevo circa 4.1-4.3 Gflop o 1,55 flop per ciclo. E no, in questo esempio -O2 non si è riavvolto in loop.
user1059432

1
Weaver ha ragione sul srotolamento ad anello, credo. Quindi probabilmente lo srotolamento manuale non è necessario
jim mcnamara il

5
Vedi l'output dell'assemblaggio sopra, non ci sono segni di srotolamento del loop.
user1059432

14
Lo srotolamento automatico migliora anche in media di 4,2 Gflop, ma richiede -funroll-loopsun'opzione che non è nemmeno inclusa in -O3. Vedere g++ -c -Q -O2 --help=optimizers | grep unroll.
user1059432

7

Utilizzando Intels icc versione 11.1 su un Intel Core 2 Duo a 2,4 GHz che ottengo

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 

Questo è molto vicino ai 9.6 Gflops ideali.

MODIFICARE:

Oops, guardando il codice assembly sembra che icc non solo abbia vettorializzato la moltiplicazione, ma abbia anche rimosso le aggiunte dal ciclo. Forzando una semantica fp più rigorosa il codice non viene più vettorializzato:

Macintosh:~ mackie$ icc -O3 -mssse3 -oaddmul addmul.cc -fp-model precise && ./addmul 1000
addmul:  0.516 s, 1.938 Gflops, res=1.326463

EDIT2:

Come richiesto:

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

Il ciclo interno del codice di clang è simile al seguente:

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

Infine, due suggerimenti: in primo luogo, se ti piace questo tipo di benchmarking, rdtscprendi in considerazione l'utilizzo dell'istruzione gettimeofday(2). È molto più preciso e fornisce il tempo in cicli, che di solito è ciò che ti interessa comunque. Per gcc e amici puoi definirlo in questo modo:

#include <stdint.h>

static __inline__ uint64_t rdtsc(void)
{
        uint64_t rval;
        __asm__ volatile ("rdtsc" : "=A" (rval));
        return rval;
}

In secondo luogo, è necessario eseguire il programma di benchmark più volte e utilizzare solo le prestazioni migliori . Nei moderni sistemi operativi accadono molte cose in parallelo, la cpu potrebbe essere in una modalità di risparmio energetico a bassa frequenza, ecc. L'esecuzione ripetuta del programma ti dà un risultato più vicino al caso ideale.


2
e che aspetto ha lo smontaggio?
Bahbar,

1
Interessante, è meno di 1 flop / ciclo. Il compilatore mescola gli addsd"e mulsd" o sono in gruppi come nell'output del mio assembly? Inoltre ottengo solo 1 flop / ciclo quando il compilatore li mescola (di cui ottengo senza -march=native). Come cambiano le prestazioni se aggiungi una riga add=mul;all'inizio della funzione addmul(...)?
user1059432

1
@ user1059432: Le istruzioni addsde subsdsono effettivamente mescolate nella versione precisa. Ho provato anche clang 3.0, non mescola le istruzioni e si avvicina molto a 2 flop / ciclo sul core 2 duo. Quando eseguo lo stesso codice sul mio laptop core i5, il mixaggio del codice non fa differenza. In entrambi i casi ricevo circa 3 flop / ciclo.
Mackie Messer,

1
@ user1059432: Alla fine si tratta di indurre il compilatore a generare codice "significativo" per un benchmark sintetico. Questo è più difficile di quanto sembri a prima vista. (es. icc supera il tuo benchmark) Se tutto ciò che vuoi è eseguire un po 'di codice a 4 flop / ciclo, la cosa più semplice è scrivere un piccolo ciclo di assemblaggio. Molto meno headake. :-)
Mackie Messer il

1
Ok, quindi ti avvicini a 2 flop / ciclo con un codice assembly simile a quello che ho citato sopra? Quanto vicino a 2? Ottengo solo 1.4, quindi è significativo. Non penso che tu abbia 3 flop / ciclo sul tuo laptop a meno che il compilatore non faccia ottimizzazioni come hai visto iccprima, puoi ricontrollare l'assemblaggio?
user1059432
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.