Jak osiągnąć teoretyczne maksimum 4 flopów na cykl?

Jak można osiągnąć teoretyczną maksymalną wydajność 4 operacji zmiennoprzecinkowych (Podwójna precyzja) na cykl na nowoczesnym procesorze Intel x86 - 64?

Z tego co rozumiem to potrzeba trzech cykli dla SSE add i pięć cykli dla mul, aby zakończyć na większości nowoczesnych procesorów Intela (patrz na przykład Agner Fog 's'Instruction Tables' ). Ze względu na pipelining można uzyskać przepustowość jednego add na cykl, jeśli algorytm ma co najmniej trzy niezależne podsumowania. Ponieważ dotyczy to zarówno wersji spakowanych addpd, jak i wersji skalarnych addsd i rejestrów SSE może zawierać dwa double, przepustowość może wynosić nawet dwa flopy na cykl.

Co więcej, wydaje się (chociaż nie widziałem odpowiedniej dokumentacji na ten temat) add i mul mogą być wykonywane równolegle, dając teoretyczną maksymalną przepustowość czterech flopów na cykl.

Jednak nie byłem w stanie odtworzyć tej wydajności za pomocą prostego programu C/C++. My best próba zaowocowała około 2.7 flops / cykl. Gdyby ktoś mógł dodać prosty program do C / C++ lub asemblera, który demonstruje szczytową wydajność, byłby bardzo mile widziany.

Moja próba:

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

Skompilowane z

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

Produkuje następujące wyjście na Intel Core i5-750, 2.66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

To znaczy, tylko o 1.4 flops na cykl. Patrząc na kod asemblera z g++ -S -O2 -march=native -masm=intel addmul.cpp główna pętla wydaje się jakby optymalne dla mnie:

.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

Zmiana wersje skalarne z spakowanymi wersjami (addpd i mulpd) podwoiłyby liczbę flopów bez zmiany czasu wykonania, więc dostałbym tylko 2.8 flopów na cykl. Czy istnieje prosty przykład, który osiąga cztery flopy na cykl?

Fajny mały program by Mysticial; oto moje wyniki (chociaż uruchom tylko przez kilka sekund):

  • gcc -O2 -march=nocona: 5.6 Gflops z 10.66 Gflops (2.1 flops/cykl)
  • cl /O2, OpenMP usunięty: 10.1 Gflops z 10.66 Gflops (3.8 flops / cykl)

To wszystko wydaje się trochę skomplikowane, ale moje wnioski jak na razie:

  • gcc -O2 zmienia kolejność niezależnych operacji zmiennoprzecinkowych z celem naprzemiennego addpd i mulpd, jeśli to możliwe. To samo dotyczy gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona wydaje się zachować kolejność operacji zmiennoprzecinkowych zdefiniowaną w źródło C++.

  • cl /O2, 64-bitowy kompilator z SDK dla Windows 7 czy rozwijanie pętli automatycznie i wydaje się próbować zorganizować operacje tak, że grupy trzech addpd's naprzemiennie z trzema mulpd' s (no, przynajmniej w moim systemie i dla mojego prostego programu).

  • My Core i5 750 (Architektura Nahelem ) nie lubi przemiennych add 'ów i mul' ów i wydaje się niemożliwy prowadzenie obu operacji równolegle. Jednak, jeśli zgrupowane w 3 to nagle działa jak magia.

  • Inne architektury (ewentualnie Sandy Bridge i inne) wydają się Możliwość równoległego uruchamiania add / mul bez problemów jeśli występują naprzemiennie w kodzie złożenia.

  • Chociaż trudno to przyznać, ale na moim systemie cl /O2 wykonuje znacznie lepszą pracę przy niskopoziomowych operacjach optymalizacyjnych dla mojego systemu i osiąga prawie szczytową wydajność dla małego przykładu C++ powyżej. Mierzyłem między 1.85-2.01 flops/cycle (użyłem clock () w Windows, co nie jest takie precyzyjne. Chyba trzeba użyć lepszego timera-dzięki Mackie Messer).

  • Najlepsze, co udało mi się zrobić gcc, to ręcznie rozwinąć pętlę i uporządkować dodawanie i mnożenie w grupach po trzy. Z g++ -O2 -march=nocona addmul_unroll.cpp Dostaję co najwyżej {[30] } co odpowiada 1.8 flopów / cykl z czego jestem teraz całkiem zadowolony.

W kodzie C++ zamieniłem pętlę for na

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

A Zgromadzenie wygląda teraz jak

.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
...
Author: Peter Mortensen, 2011-12-05

4 answers

Wykonywałem już to zadanie. Ale chodziło głównie o pomiar zużycia energii i temperatury PROCESORA. Poniższy kod (który jest dość długi) osiąga prawie optymalne na moim Core i7 2600K.

Kluczową rzeczą do odnotowania jest ogromna ilość ręcznego rozwijania pętli, jak również przeplatania mnożenia i dodawania...

Pełny projekt można znaleźć na moim Githubie: https://github.com/Mysticial/Flops

Ostrzeżenie:

Jeśli zdecydujesz się na skompiluj i uruchom to, zwróć uwagę na temperaturę procesora!!!
upewnij się, że go nie przegrzejesz. I upewnij się, że ograniczenie CPU nie wpływa na wyniki!

Ponadto, nie biorę odpowiedzialności za jakiekolwiek szkody, które mogą wyniknąć z uruchomienia tego kodu.

Uwagi:

  • ten kod jest zoptymalizowany dla x64. x86 nie ma wystarczającej ilości rejestrów, aby to dobrze skompilować.
  • ten kod został przetestowany, aby dobrze działał na Visual Studio 2010/2012 i GCC 4.6.
    ICC 11 (Intel Compiler 11) zaskakująco ma problemy z kompilacją go dobrze.
  • Są one przeznaczone dla procesorów pre-FMA. Aby osiągnąć szczytowe klapy na procesorach Intel Haswell i AMD Bulldozer (i późniejszych), potrzebne będą instrukcje FMA (Fused Multiply Add). Są one poza zakresem tego wskaźnika referencyjnego.

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

Wyjście (1 Wątek, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:

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

The maszyna jest Core i7 2600k @ 4.4 GHz. Teoretyczny szczyt SSE wynosi 4 flops * 4,4 GHz = 17,6 GFlops. Ten kod osiąga 17,3 GFlops - nieźle.

Wyjście (8 wątków, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:

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

Teoretyczny szczyt SSE to 4 flops * 4 rdzenie * 4,4 GHz = 70,4 GFlops. rzeczywista jest 65,5 GFlops.


Pójdźmy o krok dalej. 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");
}

Wyjście (1 Wątek, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:

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

Teoretyczny szczyt AVX to 8 flops * 4,4 GHz = 35,2 GFlops. Rzeczywista jest 33,4 GFlops .

Wyjście (8 wątków, 10000000 iteracji) - skompilowane z Visual Studio 2010 SP1-x64 Release:

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

Teoretyczny szczyt AVX to 8 flopów * 4 rdzenie * 4,4 GHz = 140,8 GFlops. rzeczywista jest 138,2 GFlops .


A teraz kilka wyjaśnień:

Krytyczną częścią wydajności jest oczywiście 48 instrukcji wewnątrz wewnętrznej pętli. Zauważysz, że jest podzielony na 4 bloki po 12 Instrukcji każdy. Każdy z tych 12 bloków instrukcji jest całkowicie niezależny od siebie - i zajmuje średnio 6 cykli do wykonania.

Więc jest 12 instrukcji i 6 cykli między wydaniem do użycia. Opóźnienie mnożenia wynosi 5 cykli, więc to tylko wystarczająco, aby uniknąć opóźnień.

Krok normalizacji jest potrzebny, aby dane nie przepełniały się. Jest to konieczne, ponieważ kod do-nothing powoli zwiększy / zmniejszy wielkość danych.

Więc w rzeczywistości jest to możliwe, aby zrobić lepiej niż to, jeśli po prostu użyć wszystkich zer i pozbyć się kroku normalizacji. Jednak, ponieważ napisałem benchmark do pomiaru zużycia energii i temperatury, musiałem upewnić się, że flopy są na "prawdziwych" danych, a nie zer - ponieważ jednostki wykonawcze mogą mieć specjalną obsługę przypadków dla zer, które zużywają mniej energii i wytwarzają mniej ciepła.


Więcej Wyników:

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

Wątki: 1

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

Teoretyczny szczyt SSE: 4 flops * 3,5 GHz = 14,0 GFlops . Rzeczywista jest 13,3 GFlops .

Wątki: 8

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

Teoretyczny szczyt SSE: 4 flops * 4 rdzenie * 3,5 GHz = 56,0 GFlops. Rzeczywista jest 51,3 GFlops.

mój procesor trafił w 76C na wielowątkowym biegu! Jeśli je uruchomisz, upewnij się, że Dławienie procesora nie wpływa na wyniki.


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

Wątki: 1

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

Teoretyczny szczyt SSE: 4 flops * 3,2 GHz = 12,8 GFlops . Rzeczywista jest 12,3 GFlops .

Wątki: 8

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

Teoretyczny szczyt SSE: 4 flops * 8 Rdzeni * 3,2 GHz = 102,4 GFlops . Rzeczywista jest 97,9 GFlops.

 455
Author: Mysticial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-16 16:46:41

Jest pewien punkt w architekturze Intela, o którym ludzie często zapominają, porty dyspozytorskie są współdzielone między Int i FP / SIMD. Oznacza to, że uzyskasz tylko pewną ilość impulsów FP/SIMD, zanim logika pętli utworzy pęcherzyki w strumieniu zmiennoprzecinkowym. Mystic miał więcej flopów ze swojego kodu, ponieważ używał dłuższych kroków w swojej rozwiniętej pętli.

Jeśli spojrzeć na Nehalem/Sandy Bridge architektury tutaj http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 to jasne, co się dzieje.

W przeciwieństwie do tego, powinno być łatwiej osiągnąć szczytową wydajność na AMD (Bulldozer), ponieważ rury INT i FP/SIMD mają oddzielne porty emisji z własnym schedulerem.

Jest to tylko teoretyczne, ponieważ nie mam żadnego z tych procesorów do przetestowania.

 29
Author: Patrick Schlüter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-06 16:05:20

Gałęzie mogą zdecydowanie powstrzymać cię od utrzymania szczytowej teoretycznej wydajności. Czy widzisz różnicę, jeśli ręcznie rozwijasz pętlę? Na przykład, jeśli umieścisz 5 lub 10 razy więcej ops na iterację pętli:

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
Author: TJD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-05 18:04:38

Używając Intels icc w wersji 11.1 na 2.4 GHz Intel Core 2 Duo dostaję

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 
To jest bardzo bliskie ideałowi 9.6 Gflops.

EDIT:

UPS, patrząc na kod montażu wydaje się, że icc nie tylko wektoryzował mnożenie, ale także wyciągnął dodatki z pętli. W związku z tym, że kod nie jest już wektoryzowany, kod nie jest już wektoryzowany:

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

EDIT2:

Zgodnie z życzeniem:

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

Wewnętrzna pętla kodu clanga wygląda jak to:

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

Na koniec dwie sugestie: po pierwsze, Jeśli podoba Ci się tego typu benchmarking, rozważ użycie instrukcji rdtsc z gettimeofday(2). Jest o wiele dokładniejszy i zapewnia czas w cyklach, co zwykle jest tym, co i tak cię interesuje. Dla gcc i znajomych można go zdefiniować w następujący sposób:

#include <stdint.h>

static __inline__ uint64_t rdtsc(void)
{
        uint64_t rval;
        __asm__ volatile ("rdtsc" : "=A" (rval));
        return rval;
}
Po drugie, powinieneś uruchomić swój program porównawczy kilka razy i używać tylko najlepszej wydajności . W nowoczesnych systemach operacyjnych dzieje się wiele równolegle procesor może być w trybie oszczędzania energii o niskiej częstotliwości itp. Wielokrotne uruchamianie programu daje wynik, który jest bliższy idealnemu przypadku.
 6
Author: Mackie Messer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-06 15:36:53