¿Cómo logro el máximo teórico de 4 FLOPs por ciclo?


¿Cómo se puede lograr el rendimiento máximo teórico de 4 operaciones de punto flotante (doble precisión) por ciclo en una CPU Intel x86-64 moderna?

Hasta donde yo entiendo, toma tres ciclos para un ESS add y cinco ciclos para que un mul se complete en la mayoría de las CPU Intel modernas (ver por ejemplo las 'Tablas de instrucciones'de Agner Fog ). Debido a la canalización uno puede obtener un rendimiento de uno add por ciclo si el algoritmo tiene al menos tres independientes resúmenes. Dado que esto es cierto para las versiones empaquetadas addpd, así como para las versiones escalares addsd y los registros SSE pueden contener dos double, el rendimiento puede ser tanto como dos flops por ciclo.

Además, parece (aunque no he visto ninguna documentación adecuada sobre esto) add's y mul's se pueden ejecutar en paralelo dando un rendimiento máximo teórico de cuatro flops por ciclo.

Sin embargo, no he sido capaz de replicar ese rendimiento con un simple programa C/C++. Mi mejor el intento resultó en aproximadamente 2.7 flops / ciclo. Si alguien puede contribuir con un simple programa de C/C++ o ensamblador que demuestre un rendimiento máximo, sería muy apreciado.

Mi intento:

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

Compilado con

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

Produce la siguiente salida en un Intel Core i5-750, 2.66 GHz.

addmul:  0.270 s, 3.707 Gflops, res=1.326463

Es decir, aproximadamente 1.4 flops por ciclo. Mirando el código del ensamblador con g++ -S -O2 -march=native -masm=intel addmul.cpp el bucle principal parece un poco óptima para mí:

.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

Cambiando el las versiones escalares con versiones empaquetadas (addpd y mulpd) duplicarían el número de flop sin cambiar el tiempo de ejecución, por lo que me faltarían 2,8 flops por ciclo. ¿Hay un ejemplo simple que logre cuatro flops por ciclo?

Bonito programa de Mysticial; aquí están mis resultados (ejecutar solo por unos segundos sin embargo):

  • gcc -O2 -march=nocona: 5.6 Gflops de 10.66 Gflops (2.1 flops/ciclo)
  • cl /O2, openmp eliminado: 10.1 Gflops de 10.66 Gflops (3.8 flops/ciclo)

Todo parece un poco complejo, pero mis conclusiones hasta ahora:

  • gcc -O2 cambia el orden de las operaciones independientes en coma flotante con el objetivo de alternar addpd y mulpd si es posible. Lo mismo se aplica a gcc-4.6.2 -O2 -march=core2.

  • gcc -O2 -march=nocona parece mantener el orden de las operaciones en coma flotante como se define en el código fuente de C++.

  • cl /O2, el compilador de 64 bits de la SDK para Windows 7 hace loop-desenrollar automáticamente y parece tratar de organizar las operaciones así que grupos de tres addpd's alternan con tres mulpd's (bueno, al menos en mi sistema y para mi programa simple).

  • Mi Core i5 750 (Arquitectura de Nahelem) no le gusta alternar add y mul y parece incapaz ejecutar ambas operaciones en paralelo. Sin embargo, si se agrupan en 3 de repente funciona como magia.

  • Otras arquitecturas (posiblemente Sandy Bridge y others) appear to ser capaz de ejecutar add / mul en paralelo sin problemas si se alternan en el código de ensamblaje.

  • Aunque es difícil de admitir, pero en mi sistema cl /O2 hace un trabajo mucho mejor en operaciones de optimización de bajo nivel para mi sistema y logra un rendimiento cercano al máximo para el pequeño ejemplo de C++ anterior. Medí entre 1.85-2.01 flops/cycle (han usado clock () en Windows que no es tan preciso. Supongo, necesito usar un mejor temporizador-gracias Mackie Messer).

  • Lo mejor que logré con gcc fue desenrollar y organizar manualmente adiciones y multiplicaciones en grupos de tres. Con g++ -O2 -march=nocona addmul_unroll.cpp Obtengo en el mejor de los casos 0.207s, 4.825 Gflops que corresponde a 1.8 flops/ciclo con lo cual estoy muy contento ahora.

En el código C++ he reemplazado el bucle for 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;
   }

Y la asamblea ahora se ve como

.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

He hecho esta tarea exacta antes. Pero fue principalmente para medir el consumo de energía y las temperaturas de la CPU. El siguiente código (que es bastante largo) logra cerca de óptimo en mi Core i7 2600K.

La clave a tener en cuenta aquí es la enorme cantidad de loop-unrolling manual, así como interleaving de multiplica y añade...

El proyecto completo se puede encontrar en mi GitHub: https://github.com/Mysticial/Flops

Advertencia:

Si usted decide compile y ejecute esto, preste atención a las temperaturas de su CPU!!!
Asegúrate de no sobrecalentarlo. ¡Y asegúrate de que la aceleración de la CPU no afecte tus resultados!

Además, no asumo ninguna responsabilidad por cualquier daño que pueda resultar de ejecutar este código.

Notas:

  • Este código está optimizado para x64. x86 no tiene suficientes registros para compilar bien.
  • Este código ha sido probado para funcionar bien en Visual Studio 2010/2012 y CCG 4.6.
    ICC 11 (Intel Compiler 11) sorprendentemente tiene problemas para compilarlo bien.
  • Estos son para procesadores pre-FMA. Con el fin de lograr los máximos FRACASOS en los procesadores Intel Haswell y AMD Bulldozer (y posteriores), se necesitarán instrucciones FMA (Fusionar Multiplicar). Estos están más allá del alcance de este punto de referencia.

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

Salida (1 hilo, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1-x64 Versión:

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

El la máquina es un núcleo i7 2600K @ 4.4 GHz. El pico teórico de SSE es 4 flops * 4.4 GHz = 17.6 GFlops. Este código logra 17.3 GFlops - no está mal.

Salida (8 hilos, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1-x64 Versión:

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

El pico teórico de SSE es 4 flops * 4 núcleos * 4.4 GHz = 70.4 GFlops.Actual es 65.5 GFlops.


Llevemos esto un paso más allá. 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");
}

Salida (1 hilo, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1-x64 Versión:

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

El pico AVX teórico es 8 flops * 4.4 GHz = 35.2 GFlops. Real is 33.4 GFlops.

Salida (8 hilos, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1-x64 Versión:

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

El pico AVX teórico es de 8 flops * 4 núcleos * 4.4 GHz = 140.8 GFlops. El actual es 138.2 GFlops .


Ahora para algunas explicaciones:

La parte crítica de rendimiento es obviamente las 48 instrucciones dentro del bucle interno. Notarás que está dividido en 4 bloques de 12 instrucciones cada uno. Cada uno de estos 12 bloques de instrucciones son completamente independientes entre sí - y toman en promedio 6 ciclos para ejecutarse.

Así que hay 12 instrucciones y 6 ciclos entre issue-to-use. La latencia de la multiplicación es de 5 ciclos, por lo que es sólo suficiente para evitar paradas de latencia.

El paso de normalización es necesario para evitar que los datos se desborden o desborden. Esto es necesario ya que el código de no hacer nada aumentará/disminuirá lentamente la magnitud de los datos.

Así que en realidad es posible hacer algo mejor que esto si solo usa todos los ceros y se deshace del paso de normalización. Sin embargo, desde que escribí el punto de referencia para medir el consumo de energía y la temperatura, Tuve que asegurarme de que los fracasos estuvieran en datos "reales", en lugar de ceros - ya que las unidades de ejecución pueden muy bien tener un manejo especial de casos para ceros que usan menos energía y producen menos calor.


Más resultados:

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

Hilos: 1

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

Pico teórico de SSE: 4 flops * 3.5 GHz = 14.0 GFlops. Real is 13,3 GFlops .

Hilos: 8

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

Pico teórico de SSE: 4 flops * 4 núcleos * 3.5 GHz = 56.0 GFlops. Real is 51.3 GFlops.

Mi procesador temps hit 76C en el multi-threaded run! Si ejecuta estos, asegúrese de que los resultados no se vean afectados por la limitación de la CPU.


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

Hilos: 1

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

Pico teórico de SSE: 4 flops * 3.2 GHz = 12.8 GFlops. Real is 12.3 GFlops.

Hilos: 8

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

Pico teórico de SSE: 4 flops * 8 núcleos * 3.2 GHz = 102.4 GFlops. Real is 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/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-16 16:46:41

Hay un punto en la arquitectura Intel que la gente a menudo olvida, los puertos de despacho se comparten entre Int y FP/SIMD. Esto significa que solo obtendrá una cierta cantidad de ráfagas de FP/SIMD antes de que la lógica de bucle cree burbujas en su flujo de punto flotante. Mystical sacó más fracasos de su código, porque usó zancadas más largas en su bucle desenrollado.

Si nos fijamos en la arquitectura Nehalem/Sandy Bridge aquí http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 está muy claro lo que pasa.

Por el contrario, debería ser más fácil alcanzar el máximo rendimiento en AMD (Bulldozer) ya que las tuberías INT y FP/SIMD tienen puertos de problemas separados con su propio programador.

Esto es solo teórico, ya que no tengo ninguno de estos procesadores para probar.

 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/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-06 16:05:20

Las ramas definitivamente pueden evitar que sostengas un rendimiento teórico máximo. ¿Ves una diferencia si haces manualmente un poco de desenrollado en bucle? Por ejemplo, si pones 5 o 10 veces más ops por iteración de bucle:

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/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-05 18:04:38

Usando Intels icc Versión 11.1 en un Intel Core 2 Duo de 2.4 GHz obtengo

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 

Eso está muy cerca del ideal 9.6 Gflops.

EDITAR:

Oops, mirando el código de ensamblaje parece que icc no solo vectorizó la multiplicación, sino que también sacó las adiciones del bucle. Forzando una semántica fp más estricta el código ya no se vectoriza:

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

EDIT2:

Según lo solicitado:

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

El bucle interno del código de clang se ve como esto:

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

Finalmente, dos sugerencias: Primero, si le gusta este tipo de benchmarking, considere usar la instrucción rdtsc en lugar de gettimeofday(2). Es mucho más preciso y entrega el tiempo en ciclos, que es generalmente lo que le interesa de todos modos. Para gcc y amigos puedes definirlo así:

#include <stdint.h>

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

En segundo lugar, debe ejecutar su programa de referencia varias veces y usar el solo el mejor rendimiento. En los sistemas operativos modernos suceden muchas cosas en paralelo, la cpu puede estar en un modo de ahorro de energía de baja frecuencia, etc. Ejecutar el programa repetidamente le da un resultado que está más cerca del caso ideal.

 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/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-12-06 15:36:53