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

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

Por lo que yo entiendo, tomar tres ciclos para un add SSE y cinco ciclos para un mul para completar en la mayoría de las CPU modernas de Intel (ver, por ejemplo , ‘Tablas de instrucciones’ de Agner Fog ). Debido a la canalización, se puede obtener un rendimiento de un add por ciclo si el algoritmo tiene al menos tres sums independientes. Dado que eso es cierto para packed addpd así como 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) que los add y mul puedan ejecutarse en paralelo, dando un rendimiento teórico máximo de cuatro fracasos por ciclo.

Sin embargo, no he podido replicar ese rendimiento con un simple progtwig C / C ++. Mi mejor bash resultó en 2.7 flops / ciclo. Si alguien puede contribuir con un simple C / C ++ o progtwig ensamblador que demuestre un rendimiento máximo que sería muy apreciado.

Mi bash:

 #include  #include  #include  #include  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 \n", argv[0]); printf("number of operations:  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; } 

Comstackdo 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, solo 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 algo óptimo 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 

Cambiar las versiones escalares con versiones empaquetadas ( addpd y mulpd ) duplicaría el conteo del flop sin cambiar el tiempo de ejecución, por lo que me quedaría corto de 2.8 flops por ciclo. ¿Hay un ejemplo simple que logra cuatro fracasos por ciclo?

Bonito y pequeño progtwig de Mysticial; aquí están mis resultados (aunque solo se ejecutan unos segundos):

  • gcc -O2 -march=nocona : 5.6 Gflops de 10.66 Gflops (2.1 fracasos / 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 de punto flotante con el objective 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 de punto flotante como se define en la fuente de C ++.

  • cl /O2 , el comstackdor de 64 bits del SDK para Windows 7 se desenrolla automáticamente y parece tratar de organizar operaciones para que los grupos de tres addpd alternen con tres mulpd (bueno, al menos en mi sistema y para mi progtwig simple).

  • A mi Core i5 750 ( architecture de Nahelem ) no le gustan los complementos y mul alternas y parece que no puede ejecutar ambas operaciones en paralelo. Sin embargo, si se agrupan en 3, de repente funciona como magia.

  • Otras architectures (posiblemente Sandy Bridge y otras) parecen ser capaces de ejecutar add / mul en paralelo sin problemas si se alternan en el código ensamblador.

  • 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 (he usado clock () en Windows que no es tan preciso. Supongo que necesito usar un temporizador mejor, gracias Mackie Messer).

  • Lo mejor que logré con gcc fue desenrollar en bucle manualmente y organizar adiciones y multiplicaciones en grupos de tres. Con g++ -O2 -march=nocona addmul_unroll.cpp obtengo como mucho 0.207s, 4.825 Gflops que corresponde a 1.8 flops / cycle con los que estoy bastante contento ahora.

En el código de C ++ he reemplazado el ciclo for por

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

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 casi óptimo en mi Core i7 2600K.

La cuestión clave a tener en cuenta aquí es la gran cantidad de desenrollado manual de bucles, así como el entrelazado de multiplicaciones y agrega …

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

Advertencia:

Si decide comstackr y ejecutar esto, ¡preste atención a las temperaturas de su CPU!
Asegúrate de no sobrecalentarlo. ¡Y asegúrese de que la aceleración de la CPU no afecte sus resultados!

Además, no asumo ninguna responsabilidad por el daño que pueda ocasionar la ejecución de este código.

Notas:

  • Este código está optimizado para x64. x86 no tiene suficientes registros para comstackr bien.
  • Se ha probado que este código funciona bien en Visual Studio 2010/2012 y GCC 4.6.
    ICC 11 (Intel Compiler 11) sorprendentemente tiene problemas para comstackrlo bien.
  • Estos son para procesadores anteriores a FMA. Para alcanzar los FLOPS máximos en los procesadores Intel Haswell y AMD Bulldozer (y más adelante), se necesitarán instrucciones FMA (Fused Multiply Add). Estos están más allá del scope de este punto de referencia.
 #include  #include  #include  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): comstackdo con Visual Studio 2010 SP1 - Versión x64:

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

La máquina es Core i7 2600K @ 4.4 GHz. El pico teórico de SSE es 4 fracasos * 4.4 GHz = 17.6 GFlops . Este código logra 17.3 GFlops , no está mal.

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

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

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


Avancemos un paso más. AVX ...

 #include  #include  #include  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): comstackdo con Visual Studio 2010 SP1 - Versión x64:

 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 es 33.4 GFlops .

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

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

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


Ahora para algunas explicaciones:

La parte crítica del rendimiento es, obviamente, las 48 instrucciones dentro del ciclo interno. Notarás que está dividido en 4 bloques de 12 instrucciones cada uno. Cada uno de estos 12 bloques de instrucciones es completamente independiente el uno del otro y toma un promedio de 6 ciclos para ejecutarse.

Entonces, hay 12 instrucciones y 6 ciclos entre la emisión y el uso. La latencia de la multiplicación es de 5 ciclos, por lo que es suficiente para evitar los puestos de latencia.

El paso de normalización es necesario para mantener los datos de sobre / desbordamiento. Esto es necesario ya que el código de no hacer nada boostá / disminuirá lentamente la magnitud de los datos.

Entonces, de hecho, es posible hacerlo mejor si solo utiliza 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 tenían datos "reales", en lugar de ceros , ya que las unidades de ejecución pueden tener un manejo de casos especial para ceros que usan menos energía. y produce menos calor


Más resultados:

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

Hilos: 1

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

Pico Teórico SSE: 4 fracasos * 3.5 GHz = 14.0 GFlops . Real es 13.3 GFlops .

Hilos: 8

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

Pico SSE teórico: 4 fracasos * 4 núcleos * 3.5 GHz = 56.0 GFlops . Real es 51.3 GFlops .

¡Las temperaturas de mi procesador alcanzan 76C en la ejecución de subprocesos múltiples! Si los ejecuta, asegúrese de que los resultados no se vean afectados por la aceleración de la CPU.


  • 2 x Intel Xeon X5482 Harpertown a 3.2 GHz
  • Ubuntu Linux 10 x64
  • GCC 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 SSE: 4 fracasos * 3.2 GHz = 12.8 GFlops . Real es 12.3 GFlops .

Hilos: 8

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

Pico Teórico SSE: 4 fracasos * 8 núcleos * 3.2 GHz = 102.4 GFlops . Actual es 97.9 GFlops .

Hay un punto en la architecture de 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 obtuvo más fracasos de su código, porque usó zancadas más largas en su ciclo desenrollado.

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

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

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

Las sucursales definitivamente pueden evitar que usted mantenga el rendimiento teórico máximo. ¿Ves una diferencia si manualmente se desenrolla? Por ejemplo, si coloca 5 o 10 veces más operaciones por iteración de bucle:

 for(int i=0; i 

Usando la versión 11.1 de Intels icc en un Intel Core 2 Duo de 2.4GHz, me sale

 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:

Vaya, mirando el código de ensamblado, parece que icc no solo vectorizó la multiplicación, sino que también extrajo las adiciones del ciclo. 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:

De acuerdo a lo pedido:

 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 ciclo interno del código de clang se ve así:

  .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 evaluación comparativa, considere usar la instrucción rdtsc en lugar de gettimeofday gettimeofday(2) . Es mucho más preciso y ofrece el tiempo en ciclos, que generalmente es lo que le interesa de todos modos. Para gcc y amigos puedes definirlo así:

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

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