c++ performance gcc sse avx

c++ - Usar AVX intrinsics en lugar de SSE no mejora la velocidad, ¿por qué?



performance gcc (4)

He estado usando los intrínsecos de SSE de Intel durante bastante tiempo con buenas ganancias de rendimiento. Por lo tanto, esperaba que los intrínsecos de AVX aceleraran aún más mis programas. Esto, desafortunadamente, no fue el caso hasta ahora. Probablemente estoy cometiendo un error estúpido, así que estaría muy agradecido si alguien pudiera ayudarme.

Yo uso Ubuntu 11.10 con g ++ 4.6.1. Recopilé mi programa (ver abajo) con

g++ simpleExample.cpp -O3 -march=native -o simpleExample

El sistema de prueba tiene una CPU Intel i7-2600.

Aquí está el código que ejemplifica mi problema. En mi sistema, obtengo la salida

98.715 ms, b[42] = 0.900038 // Naive 24.457 ms, b[42] = 0.900038 // SSE 24.646 ms, b[42] = 0.900038 // AVX

Tenga en cuenta que el cálculo sqrt (sqrt (sqrt (x))) solo se eligió para garantizar que el ancho de banda de la memoria no limite la velocidad de ejecución; es solo un ejemplo.

simpleExample.cpp:

#include <immintrin.h> #include <iostream> #include <math.h> #include <sys/time.h> using namespace std; // ----------------------------------------------------------------------------- // This function returns the current time, expressed as seconds since the Epoch // ----------------------------------------------------------------------------- double getCurrentTime(){ struct timeval curr; struct timezone tz; gettimeofday(&curr, &tz); double tmp = static_cast<double>(curr.tv_sec) * static_cast<double>(1000000) + static_cast<double>(curr.tv_usec); return tmp*1e-6; } // ----------------------------------------------------------------------------- // Main routine // ----------------------------------------------------------------------------- int main() { srand48(0); // seed PRNG double e,s; // timestamp variables float *a, *b; // data pointers float *pA,*pB; // work pointer __m128 rA,rB; // variables for SSE __m256 rA_AVX, rB_AVX; // variables for AVX // define vector size const int vector_size = 10000000; // allocate memory a = (float*) _mm_malloc (vector_size*sizeof(float),32); b = (float*) _mm_malloc (vector_size*sizeof(float),32); // initialize vectors // for(int i=0;i<vector_size;i++) { a[i]=fabs(drand48()); b[i]=0.0f; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // Naive implementation // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ s = getCurrentTime(); for (int i=0; i<vector_size; i++){ b[i] = sqrtf(sqrtf(sqrtf(a[i]))); } e = getCurrentTime(); cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl; // ----------------------------------------------------------------------------- for(int i=0;i<vector_size;i++) { b[i]=0.0f; } // ----------------------------------------------------------------------------- // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // SSE2 implementation // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pA = a; pB = b; s = getCurrentTime(); for (int i=0; i<vector_size; i+=4){ rA = _mm_load_ps(pA); rB = _mm_sqrt_ps(_mm_sqrt_ps(_mm_sqrt_ps(rA))); _mm_store_ps(pB,rB); pA += 4; pB += 4; } e = getCurrentTime(); cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl; // ----------------------------------------------------------------------------- for(int i=0;i<vector_size;i++) { b[i]=0.0f; } // ----------------------------------------------------------------------------- // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // AVX implementation // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pA = a; pB = b; s = getCurrentTime(); for (int i=0; i<vector_size; i+=8){ rA_AVX = _mm256_load_ps(pA); rB_AVX = _mm256_sqrt_ps(_mm256_sqrt_ps(_mm256_sqrt_ps(rA_AVX))); _mm256_store_ps(pB,rB_AVX); pA += 8; pB += 8; } e = getCurrentTime(); cout << (e-s)*1000 << " ms" << ", b[42] = " << b[42] << endl; _mm_free(a); _mm_free(b); return 0; }

¡Cualquier ayuda es apreciada!


Dependiendo del hardware de su procesador, las instrucciones AVX pueden emularse en el hardware como instrucciones SSE. Debería buscar el número de pieza de su procesador para obtener las especificaciones exactas, pero esta es una de las principales diferencias entre los procesadores de intel de gama baja y alta, la cantidad de unidades de ejecución especializadas frente a la emulación de hardware.


Esto se debe a que VSQRTPS (instrucción AVX) lleva exactamente el doble de ciclos que SQRTPS (instrucción SSE) en un procesador Sandy Bridge. Consulte la guía de optimización de Agner Fog: tablas de instrucciones , página 88.

Las instrucciones como raíz cuadrada y división no se benefician con AVX. Por otro lado, adiciones, multiplicaciones, etc., sí.


Si está interesado en aumentar el rendimiento de la raíz cuadrada, en lugar de VSQRTPS puede usar VRSQRTPS y la fórmula de Newton-Raphson:

x0 = vrsqrtps(a) x1 = 0.5 * x0 * (3 - (a * x0) * x0)

VRSQRTPS en sí mismo no se beneficia de AVX, pero otros cálculos sí lo hacen.

Úselo si 23 bits de precisión son suficientes para usted.


Solo por completitud. La implementación de Newton-Raphson (NR) para operaciones como la división o la raíz cuadrada solo será beneficiosa si tienes un número limitado de esas operaciones en tu código. Esto se debe a que si utilizó estos métodos alternativos generará más presión en otros puertos, como los puertos de multiplicación y adición. Esa es básicamente la razón por la que las arquitecturas x86 tienen una unidad de hardware especial para manejar estas operaciones en lugar de las soluciones de software alternativas (como NR). Cito de Intel 64 y IA-32 Architectures Optimization Reference Manual p.556:

"En algunos casos, cuando las operaciones de división o raíz cuadrada son parte de un algoritmo mayor que oculta parte de la latencia de estas operaciones, la aproximación con Newton-Raphson puede ralentizar la ejecución".

Por lo tanto, tenga cuidado al usar NR en algoritmos grandes. En realidad, tuve mi tesis de maestría sobre este punto y dejaré un enlace aquí para futuras referencias, una vez que se publique.

También para las personas, cómo siempre se preguntan sobre el rendimiento y la latencia de ciertas instrucciones, eche un vistazo a IACA . Es una herramienta muy útil proporcionada por Intel para analizar estáticamente el rendimiento de ejecución de los códigos en el núcleo.

editado aquí es un enlace a la tesis para aquellos que están interesados thesis