it-swarm.com.de

Wie erreiche ich das theoretische Maximum von 4 FLOPs pro Zyklus?

Wie kann die theoretische Spitzenleistung von 4 Gleitkommaoperationen (doppelte Genauigkeit) pro Zyklus auf einer modernen x86-64-Intel-CPU erreicht werden?

Soweit ich weiß, dauert es drei Zyklen, bis ein SSE add und fünf Zyklen, bis ein mul auf den meisten abgeschlossen ist der modernen Intel CPUs (siehe zum Beispiel Agner Fog's 'Instruction Tables' ). Aufgrund von Pipelining kann man einen Durchsatz von einem add pro Zyklus erhalten, wenn der Algorithmus mindestens drei unabhängige Summierungen aufweist. Da dies sowohl für gepackte addpd als auch für die skalaren addsd Versionen gilt und SSE Register zwei double enthalten können, kann der Durchsatz wie folgt sein bis zu zwei Flops pro Zyklus.

Darüber hinaus scheinen (obwohl ich keine ordnungsgemäße Dokumentation dazu gesehen habe) add und mul parallel ausgeführt werden zu können, was einen theoretischen maximalen Durchsatz von vier Flops pro Zyklus ergibt.

Ich war jedoch nicht in der Lage, diese Leistung mit einem einfachen C/C++ - Programm zu replizieren. Mein bester Versuch ergab ungefähr 2,7 Flops/Zyklus. Wenn irgendjemand ein einfaches C/C++ - oder Assembler-Programm beisteuern kann, das Spitzenleistung demonstriert, wäre das sehr dankbar.

Mein Versuch:

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

Kompiliert mit

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

erzeugt die folgende Ausgabe auf einem Intel Core i5-750 mit 2,66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Das heißt, nur ungefähr 1,4 Flops pro Zyklus. Betrachten Sie den Assembler-Code mit g++ -S -O2 -march=native -masm=intel addmul.cpp Die Hauptschleife scheint mir irgendwie optimal zu sein:

.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

Das Ändern der Skalarversionen mit gepackten Versionen (addpd und mulpd) würde die Anzahl der Flops verdoppeln, ohne die Ausführungszeit zu ändern, und ich würde nur knapp 2,8 Flops pro Zyklus erhalten. Gibt es ein einfaches Beispiel, das vier Flops pro Zyklus erzielt?

Nettes kleines Programm von Mysticial; Hier sind meine Ergebnisse (allerdings nur ein paar Sekunden):

  • gcc -O2 -march=nocona: 5,6 Gflops von 10,66 Gflops (2,1 Flops/Zyklus)
  • cl /O2, openmp entfernt: 10.1 Flops von 10.66 Gflops (3.8 Flops/Zyklus)

Es scheint alles ein bisschen komplex, aber meine Schlussfolgerungen bisher:

  • gcc -O2 ändert die Reihenfolge unabhängiger Gleitkommaoperationen mit dem Ziel, addpd und mulpd nach Möglichkeit zu wechseln. Gleiches gilt für gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona scheint die in der C++ - Quelle definierte Reihenfolge der Gleitkommaoperationen beizubehalten.

  • cl /O2, der 64-Bit-Compiler aus SDK für Windows 7 führt das Abrollen der Schleife automatisch durch und scheint die Vorgänge so anzuordnen, dass sich Gruppen von drei addpd mit drei mulpd 's (na ja, zumindest auf meinem System und für mein einfaches Programm).

  • Mein Core i5 75 ( Nehalem-Architektur ) mag keine abwechselnden Adds und Muls und scheint nicht in der Lage zu sein, beide Operationen gleichzeitig auszuführen. Wenn es jedoch in Dreien gruppiert ist, funktioniert es plötzlich wie Magie.

  • Andere Architekturen (möglicherweise Sandy Bridge und andere) scheinen add/mul problemlos parallel ausführen zu können, wenn sie sich im Assembly-Code abwechseln.

  • Obwohl schwer zuzugeben, aber auf meinem System cl /O2 erledigt einen viel besseren Job bei Optimierungsvorgängen auf niedriger Ebene für mein System und erzielt im obigen kleinen C++ - Beispiel eine nahezu maximale Leistung. Ich habe zwischen 1,85 und 2,11 Flops/Zyklus gemessen (habe clock () in Windows verwendet, was nicht so genau ist. Ich denke, ich muss einen besseren Timer verwenden - danke an Mackie Messer).

  • Das Beste, was ich mit gcc geschafft habe, war das manuelle Abrollen von Loops und das Anordnen von Additionen und Multiplikationen in Dreiergruppen. Mit g++ -O2 -march=nocona addmul_unroll.cpp Ich komme am besten 0.207s, 4.825 Gflops das entspricht 1,8 Flops/Zyklus, mit denen ich jetzt ganz zufrieden bin.

Im C++ Code habe ich die for Schleife durch ersetzt

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

Und die Versammlung sieht jetzt so aus

.L4:
mulsd    xmm8, xmm3
mulsd    xmm7, xmm3
mulsd    xmm6, xmm3
addsd    xmm13, xmm2
addsd    xmm12, xmm2
addsd    xmm11, xmm2
mulsd    xmm5, xmm3
mulsd    xmm1, xmm3
mulsd    xmm8, xmm3
addsd    xmm10, xmm2
addsd    xmm9, xmm2
addsd    xmm13, xmm2
...
609
user1059432

Ich habe genau diese Aufgabe schon einmal erledigt. Es wurde jedoch hauptsächlich der Stromverbrauch und die CPU-Temperaturen gemessen. Der folgende Code (der ziemlich lang ist) erreicht auf meinem Core i7 2600K nahezu das Optimum.

Das Wichtigste dabei ist der enorme Aufwand beim manuellen Abrollen von Loops sowie beim Verschachteln von Multiplikationen und Additionen.

Das vollständige Projekt finden Sie auf meinem GitHub: https://github.com/Mysticial/Flops

Warnung:

Wenn Sie sich entscheiden, dies zu kompilieren und auszuführen, achten Sie auf Ihre CPU-Temperaturen !!!
Stellen Sie sicher, dass Sie es nicht überhitzen. Und stellen Sie sicher, dass die CPU-Drosselung Ihre Ergebnisse nicht beeinträchtigt!

Darüber hinaus übernehme ich keine Verantwortung für Schäden, die durch die Ausführung dieses Codes entstehen können.

Anmerkungen:

  • Dieser Code ist für x64 optimiert. x86 verfügt nicht über genügend Register, damit dies gut kompiliert werden kann.
  • Dieser Code wurde für die einwandfreie Funktion in Visual Studio 2010/2012 und GCC 4.6 getestet.
    ICC 11 (Intel Compiler 11) hat überraschenderweise Probleme, es gut zu kompilieren.
  • Diese sind für Pre-FMA-Prozessoren. Um die Spitze FLOPS auf Intel Haswell- und AMD Bulldozer-Prozessoren (und höher) zu erreichen, sind FMA-Anweisungen (Fused Multiply Add) erforderlich. Dies würde den Rahmen dieser Benchmark sprengen.
#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");
}

Ausgabe (1 Thread, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:

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

Die Maschine ist ein Core i7 2600K bei 4,4 GHz. Der theoretische SSE Peak beträgt 4 Flops * 4,4 GHz = 17,6 GFlops . Dieser Code erreicht 17.3 GFlops - nicht schlecht.

Ausgabe (8 Threads, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:

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

Theoretischer SSE Peak ist 4 Flops * 4 Kerne * 4,4 GHz = 70,4 GFlops. Tatsächlich ist 65,5 GFlops .


Gehen wir noch einen Schritt weiter. 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");
}

Ausgabe (1 Thread, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:

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

Der theoretische AVX-Peak beträgt 8 Flops * 4,4 GHz = 35,2 GFlops . Aktuell ist 33.4 GFlops .

Ausgabe (8 Threads, 10000000 Iterationen) - Kompiliert mit Visual Studio 2010 SP1 - x64 Release:

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

Der theoretische AVX-Peak beträgt 8 Flops * 4 Kerne * 4,4 GHz = 140,8 GFlops. Der tatsächliche Wert beträgt 138,2 GFlops .


Nun zu einigen Erklärungen:

Der leistungskritische Teil sind offensichtlich die 48 Anweisungen in der inneren Schleife. Sie werden feststellen, dass es in 4 Blöcke mit jeweils 12 Anweisungen unterteilt ist. Jeder dieser 12 Anweisungsblöcke ist völlig unabhängig voneinander - und benötigt durchschnittlich 6 Zyklen, um ausgeführt zu werden.

Es gibt also 12 Anweisungen und 6 Zyklen zwischen der Ausgabe und der Verwendung. Die Multiplikationsverzögerung beträgt 5 Zyklen, es ist also gerade genug, um Wartezeiten zu vermeiden.

Der Normalisierungsschritt ist erforderlich, um ein Über-/Unterlaufen der Daten zu verhindern. Dies ist erforderlich, da der Do-Nothing-Code die Größe der Daten langsam erhöht/verringert.

Es ist also tatsächlich möglich, ein besseres Ergebnis zu erzielen, wenn Sie nur alle Nullen verwenden und den Normalisierungsschritt loswerden. Da ich jedoch den Benchmark zur Messung von Stromverbrauch und Temperatur geschrieben habe , musste ich sicherstellen, dass sich die Flops auf "echte" Daten und nicht auf Nullen befanden - wie es die Ausführungseinheiten sehr wohl haben könnten Sonderfallbehandlung für Nullen, die weniger Strom verbrauchen und weniger Wärme produzieren.


Mehr Ergebnisse:

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

Themen: 1

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

Theoretischer SSE Peak: 4 Flops * 3,5 GHz = 14,0 GFlops . Aktuell ist 13.3 GFlops .

Themen: 8

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

Theoretischer SSE Peak: 4 Flops * 4 Kerne * 3,5 GHz = 56,0 GFlops . Aktuell ist 51.3 GFlops .

Meine Prozessor-Temps haben beim Multithread-Lauf 76C erreicht! Wenn Sie diese ausführen, stellen Sie sicher, dass die Ergebnisse nicht von der CPU-Drosselung betroffen sind.


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

Themen: 1

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

Theoretischer SSE Peak: 4 Flops * 3,2 GHz = 12,8 GFlops . Aktuell ist 12.3 GFlops .

Themen: 8

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

Theoretischer SSE Peak: 4 Flops * 8 Kerne * 3,2 GHz = 102,4 GFlops . Aktuell ist 97.9 GFlops .

496
Mysticial

Es gibt einen Punkt in der Intel-Architektur, den die Leute oft vergessen: Die Dispatch-Ports werden von Int und FP/SIMD gemeinsam genutzt. Dies bedeutet, dass Sie nur eine bestimmte Anzahl von FP/SIMD-Bursts erhalten, bevor die Schleifenlogik Blasen in Ihrem Gleitkommastream erzeugt. Mystical holte mehr Flops aus seinem Code, weil er in seiner entrollten Schleife längere Schritte benutzte.

Wenn Sie sich die Architektur von Nehalem/Sandy Bridge hier ansehen http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 , ist es ganz schön klar was passiert.

Im Gegensatz dazu sollte es unter AMD (Bulldozer) einfacher sein, eine Spitzenleistung zu erzielen, da die INT- und FP/SIMD-Pipes separate Issue-Ports mit einem eigenen Scheduler haben.

Dies ist nur theoretisch, da ich keinen dieser Prozessoren zum Testen habe.

32

Branchen können Sie definitiv davon abhalten, theoretische Höchstleistungen zu erbringen. Sehen Sie einen Unterschied, wenn Sie manuell eine Schleife abrollen? Wenn Sie beispielsweise fünf- oder zehnmal so viele Operationen pro Schleifeniteration ausführen:

for(int i=0; i<loops/5; i++) {
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
      mul1*=mul; mul2*=mul; mul3*=mul; mul4*=mul; mul5*=mul;
      sum1+=add; sum2+=add; sum3+=add; sum4+=add; sum5+=add;
   }
16
TJD

Mit Intel ICC Version 11.1 auf einem 2,4 GHz Intel Core 2 Duo bekomme ich

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 

Das ist sehr nahe an den idealen 9.6 Gflops.

BEARBEITEN:

Hoppla, im Assembly-Code sieht es so aus, als hätte icc nicht nur die Multiplikation vektorisiert, sondern auch die Additionen aus der Schleife gezogen. Durch Erzwingen einer strengeren fp-Semantik wird der Code nicht mehr vektorisiert:

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

EDIT2:

Wie gewünscht:

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

Die innere Schleife von Clangs Code sieht folgendermaßen aus:

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

Abschließend zwei Vorschläge: Erstens, wenn Sie diese Art von Benchmarking mögen, sollten Sie die Anweisung rdtsc anstelle von gettimeofday(2) verwenden. Es ist viel genauer und liefert die Zeit in Zyklen, woran Sie normalerweise sowieso interessiert sind. Für gcc und friends kannst du es so definieren:

#include <stdint.h>

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

Zweitens sollten Sie Ihr Benchmark-Programm mehrmals ausführen und nur bei bester Leistung verwenden. In modernen Betriebssystemen passieren viele Dinge parallel, die CPU befindet sich möglicherweise in einem Niedrigfrequenz-Energiesparmodus usw. Wenn Sie das Programm wiederholt ausführen, erhalten Sie ein Ergebnis, das dem Idealfall näher kommt.

7
Mackie Messer