2011-10-27 23 views
13

Esta publicación está estrechamente relacionada con otra que publiqué some days ago. Esta vez, escribí un código simple que simplemente agrega un par de matrices de elementos, multiplica el resultado por los valores en otra matriz y lo almacena en una cuarta matriz, todas las variables de precisión doble de tipo punto flotante.GCC SSE código de optimización

Hice dos versiones de ese código: uno con instrucciones SSE, usando llamadas ay otro sin ellas, luego las compilé con gcc y -O0 nivel de optimización. Las escribo a continuación:

// SSE VERSION 

#define N 10000 
#define NTIMES 100000 
#include <time.h> 
#include <stdio.h> 
#include <xmmintrin.h> 
#include <pmmintrin.h> 

double a[N] __attribute__((aligned(16))); 
double b[N] __attribute__((aligned(16))); 
double c[N] __attribute__((aligned(16))); 
double r[N] __attribute__((aligned(16))); 

int main(void){ 
    int i, times; 
    for(times = 0; times < NTIMES; times++){ 
    for(i = 0; i <N; i+= 2){ 
     __m128d mm_a = _mm_load_pd(&a[i]); 
     _mm_prefetch(&a[i+4], _MM_HINT_T0); 
     __m128d mm_b = _mm_load_pd(&b[i]); 
     _mm_prefetch(&b[i+4] , _MM_HINT_T0); 
     __m128d mm_c = _mm_load_pd(&c[i]); 
     _mm_prefetch(&c[i+4] , _MM_HINT_T0); 
     __m128d mm_r; 
     mm_r = _mm_add_pd(mm_a, mm_b); 
     mm_a = _mm_mul_pd(mm_r , mm_c); 
     _mm_store_pd(&r[i], mm_a); 
     } 
    } 
} 

//NO SSE VERSION 
//same definitions as before 
int main(void){ 
    int i, times; 
    for(times = 0; times < NTIMES; times++){ 
    for(i = 0; i < N; i++){ 
     r[i] = (a[i]+b[i])*c[i]; 
    } 
    } 
} 

Al compilar con -O0, gcc hace uso de registros XMM/MMX y SSE intstructions, si no se da específicamente el opciones -mno-sse (y otros). Inspeccioné el código de ensamblado generado para el segundo código y noté que usa movsd, suma y mulsd instrucciones. Por lo tanto, utiliza las instrucciones de SSE, pero solo las que usan la parte más baja de los registros, si no estoy equivocado. El código de ensamblado generado para el primer código C hizo uso, como se esperaba, de las instrucciones addp y mulpd, aunque se generó un código de ensamblaje bastante más grande.

De todos modos, el primer código debería obtener un mejor beneficio, hasta donde yo sé, del paradigma SIMD, ya que cada iteración dos valores de resultado se calculan. Aún así, el segundo código realiza algo como un 25 por ciento más rápido que el primero. También realicé una prueba con valores de precisión simples y obtuve resultados similares. ¿Cuál es la razón para eso?

+5

Comparar el rendimiento al compilar sin optimizaciones es bastante insignificante. – interjay

+1

Está realizando 3 cargas xy 1 x tienda solo por 2 operaciones aritméticas, por lo que lo más probable es que tenga un ancho de banda limitado. –

+5

¿Qué sucede cuando elimina las llamadas _mm_prefetch? Creo que pueden lastimarte – TJD

Respuesta

14

La vectorización en GCC está habilitada en -O3. Es por eso que en -O0, solo verá las instrucciones SSE2 escalares ordinarias (movsd, addsd, etc.). El uso de GCC 4.6.1 y el segundo ejemplo:

#define N 10000 
#define NTIMES 100000 

double a[N] __attribute__ ((aligned (16))); 
double b[N] __attribute__ ((aligned (16))); 
double c[N] __attribute__ ((aligned (16))); 
double r[N] __attribute__ ((aligned (16))); 

int 
main (void) 
{ 
    int i, times; 
    for (times = 0; times < NTIMES; times++) 
    { 
     for (i = 0; i < N; ++i) 
     r[i] = (a[i] + b[i]) * c[i]; 
    } 

    return 0; 
} 

y compilar con gcc -S -O3 -msse2 sse.c produce para el bucle interno las siguientes instrucciones, que es bastante bueno:

.L3: 
    movapd a(%eax), %xmm0 
    addpd b(%eax), %xmm0 
    mulpd c(%eax), %xmm0 
    movapd %xmm0, r(%eax) 
    addl $16, %eax 
    cmpl $80000, %eax 
    jne .L3 

Como se puede ver, con la vectorización habilitado GCC emite código para realizar dos iteraciones de bucle en paralelo. Sin embargo, se puede mejorar: este código usa los 128 bits inferiores de los registros SSE, pero puede usar todos los registros YMM de 256 bits, habilitando la codificación AVX de las instrucciones SSE (si están disponibles en la máquina). Por lo tanto, la compilación del mismo programa con gcc -S -O3 -msse2 -mavx sse.c da para el bucle interno:

.L3: 
    vmovapd a(%eax), %ymm0 
    vaddpd b(%eax), %ymm0, %ymm0 
    vmulpd c(%eax), %ymm0, %ymm0 
    vmovapd %ymm0, r(%eax) 
    addl $32, %eax 
    cmpl $80000, %eax 
    jne .L3 

Tenga en cuenta que v delante de cada instrucción y que las instrucciones de uso de los 256 bits YMM registra, cuatro iteraciones del bucle original, se ejecutan en paralelo.

+0

Acabo de ejecutar esto a través de 'gcc 4.7.2' en' x86-64' con y sin las banderas '-msse2' - ambas dieron como resultado la misma salida de ensamblador. Entonces, ¿sería seguro que las instrucciones estén habilitadas por defecto en esta plataforma? –

+0

@lori, sí, SSE está predeterminado en x86-64. – chill

+0

Aquí puede consultar con diferentes compiladores http://goo.gl/bM62CZ – KindDragon

2

Me gustaría extender chill's answer y llamar su atención sobre el hecho de que GCC parece no ser capaz de hacer el mismo uso inteligente de las instrucciones AVX cuando itera hacia atrás.

basta con sustituir el bucle interno en el código de ejemplo de relajarse con:

for (i = N-1; i >= 0; --i) 
    r[i] = (a[i] + b[i]) * c[i]; 

GCC (4.8.4) con las opciones -S -O3 -mavx produce:

.L5: 
    vmovsd a+79992(%rax), %xmm0 
    subq $8, %rax 
    vaddsd b+80000(%rax), %xmm0, %xmm0 
    vmulsd c+80000(%rax), %xmm0, %xmm0 
    vmovsd %xmm0, r+80000(%rax) 
    cmpq $-80000, %rax 
    jne  .L5 
+0

Interesante. El gcc más nuevo se auto-vectoriza hilarantemente, usando 'vpermpd 0b00011011' para cada matriz de entrada/salida para invertirlo después de cargarlo, de modo que los elementos de datos dentro de cada vector van del primero al último en orden de fuente. ¡Eso es 4 'vpermpd's por iteración! Curiosamente, [clang lo auto-vectoriza muy bien] (https://godbolt.org/g/azbIIi) –

Cuestiones relacionadas