c++ - ¿Cómo logro el máximo teórico de 4 FLOP por ciclo?
optimization architecture (4)
Al usar Intels icc versión 11.1 en un procesador Intel Core 2 Duo de 2.4GHz, 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 de los ideales 9.6 Gflops.
EDITAR:
Vaya, mirando el código de ensamblaje parece que icc no solo vectorizó la multiplicación, sino que también eliminó las adiciones del bucle. Forzando una semántica fp más estricta, el código ya no está vectorizado:
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 bucle 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, lo que suele ser 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 solo el mejor rendimiento . 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. La ejecución del programa repetidamente le da un resultado que está más cerca del caso ideal.
¿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?
Según tengo entendido, se requieren tres ciclos para un add
SSE y cinco ciclos para que un mul
complete en la mayoría de las CPU de Intel modernas (consulte, por ejemplo, las "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 sumas independientes. Dado que esto es cierto para el addpd
empaquetado, así como las versiones de escalada addsd
y SSE pueden contener dos double
, el rendimiento puede ser de hasta dos fracasos por ciclo.
Además, parece que (aunque no he visto ninguna documentación adecuada sobre esto) los add
''s y los mul
'' s pueden ejecutarse en paralelo dando un rendimiento teórico máximo de cuatro flops por ciclo.
Sin embargo, no he podido replicar ese rendimiento con un simple programa C / C ++. Mi mejor intento resultó en aproximadamente 2.7 fracasos / ciclos. Si alguien puede contribuir con un simple C / C ++ o un programa ensamblador que demuestre un rendimiento máximo, esto 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, casi 1.4 fracasos por ciclo. Mirando el código del ensamblador con g++ -S -O2 -march=native -masm=intel addmul.cpp
el bucle principal me parece algo óptimo:
.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 de flop sin cambiar el tiempo de ejecución y, por lo tanto, me mulpd
2.8 flops por ciclo. ¿Hay un ejemplo simple que logre cuatro fracasos por ciclo?
Buen programa por Mysticial; Aquí están mis resultados (correr solo por unos segundos):
-
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 de punto flotante independientes con el objetivo de alternaraddpd
ymulpd
''s si es posible. Lo mismo se aplica agcc-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 compilador de 64 bits del SDK para Windows 7 se desenrolla automáticamente y parece intentar organizar operaciones para que los grupos de tresaddpd
alternen con los tresmulpd
(bueno, al menos en mi sistema y para mi programa simple).My Core i5 750 ( arquitectura Nahelem ) no le gusta alternar add and mul''s y parece que no puede ejecutar ambas operaciones en paralelo. Sin embargo, si se agrupan en 3, de repente funciona como magia.
Otras arquitecturas (posiblemente Sandy Bridge y otras) parecen poder 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 las operaciones de optimización de bajo nivel para mi sistema y alcanza el rendimiento máximo para el pequeño ejemplo de C ++ anterior. Medí entre 1.85-2.01 flops / ciclo (he usado clock () en Windows, lo cual no es tan preciso. Supongo que necesito usar un temporizador mejor, gracias Mackie Messer).Lo mejor que
gcc
congcc
fue enrollar y enrollar manualmente las adiciones y multiplicaciones en grupos de tres. Cong++ -O2 -march=nocona addmul_unroll.cpp
obtengo, en el mejor de los0.207s, 4.825 Gflops
que corresponde a 1.8 fracasos / ciclos con los que estoy bastante 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 parece
.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
...
Hay un punto en la arquitectura de Intel que la gente suele olvidar, 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 del bucle cree burbujas en su flujo de punto flotante. Mystical obtuvo más fracasos de su código, porque utilizó pasos más largos en su bucle desenrollado.
Si observa la arquitectura de Nehalem / Sandy Bridge aquí http://www.realworldtech.com/page.cfm?ArticleID=RWT091810191937&p=6 queda bastante claro lo que sucede.
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 emisión separados con su propio programador.
Esto es solo teórico ya que no tengo ninguno de estos procesadores para probar.
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) alcanza un nivel casi óptimo en mi Core i7 2600K.
La clave a tener en cuenta aquí es la cantidad masiva de desenrollado manual de bucle, así como el entrelazado de multiplicaciones y agregados ...
El proyecto completo se puede encontrar en mi GitHub: https://github.com/Mysticial/Flops
Advertencia:
Si decides compilar y ejecutar esto, ¡presta atención a las temperaturas de tu 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 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 que esto se compile bien.
- Este código ha sido probado para que funcione bien en Visual Studio 2010/2012 y GCC 4.6.
ICC 11 (Intel Compiler 11) sorprendentemente tiene problemas para compilarlo bien. - Estos son para procesadores pre-FMA. Para alcanzar los FLOPS máximos en los procesadores Intel Haswell y AMD Bulldozer (y versiones posteriores), se necesitarán las instrucciones FMA (Fused Multiply Add). 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 subproceso, 10000000 iteraciones): compilado con Visual Studio 2010 SP1 - x64 Release:
Seconds = 55.5104
FP Ops = 960000000000
FLOPs = 1.7294e+010
sum = 2.22652
La máquina es un Core i7 2600K @ 4.4 GHz. El pico teórico de la ESS es de 4 fracasos * 4,4 GHz = 17,6 GFlops . Este código logra 17.3 GFlops - no está mal.
Salida (8 subprocesos, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1 - x64 Release:
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. Actual es 65.5 GFlops .
Vamos a dar un paso más. 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 subproceso, 10000000 iteraciones): compilado con Visual Studio 2010 SP1 - x64 Release:
Seconds = 57.4679
FP Ops = 1920000000000
FLOPs = 3.34099e+010
sum = 4.45305
El pico teórico de AVX es 8 fracasos * 4,4 GHz = 35,2 GFlops . Actual es 33.4 GFlops .
Salida (8 subprocesos, 10000000 iteraciones) - Compilado con Visual Studio 2010 SP1 - x64 Release:
Seconds = 111.119
FP Ops = 15360000000000
FLOPs = 1.3823e+011
sum = 35.6244
El pico AVX teórico es de 8 fracasos * 4 núcleos * 4,4 GHz = 140,8 GFlops. Actual es 138.2 GFlops .
Ahora para algunas explicaciones:
La parte crítica del 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 es completamente independiente entre sí, y toma en promedio 6 ciclos para ejecutarse.
Así que 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 bloqueos 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.
Por lo tanto, es posible hacerlo mejor si solo usas todos los ceros y eliminas el paso de la 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 fueran sobre datos "reales", en lugar de ceros , ya que las unidades de ejecución pueden tener un manejo especial de casos para ceros que usan menos energía y producir menos calor.
Más resultados:
- Intel Core i7 920 a 3.5 GHz
- Windows 7 Ultimate x64
- Visual Studio 2010 SP1 - Lanzamiento x64
Hilos: 1
Seconds = 72.1116
FP Ops = 960000000000
FLOPs = 1.33127e+010
sum = 2.22652
Pico SSE teórico: 4 flops * 3.5 GHz = 14.0 GFlops . Actual 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 . Actual es 51.3 GFlops .
¡Mis temperaturas de procesador llegaron a 76C en la ejecución de subprocesos múltiples! Si ejecuta estos, 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 SSE teórico: 4 flops * 3.2 GHz = 12.8 GFlops . Actual es 12.3 GFlops .
Hilos: 8
Seconds = 78.4733
FP Ops = 7680000000000
FLOPs = 9.78676e+10
sum = 17.8122
Pico teórico de SSE: 4 fracasos * 8 núcleos * 3.2 GHz = 102.4 GFlops . Actual es 97.9 GFlops .
Las sucursales definitivamente pueden evitar que mantengas el rendimiento teórico máximo. ¿Ves una diferencia si realizas manualmente el desenrollado de un bucle? Por ejemplo, si coloca 5 o 10 veces más operaciones 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;
}