2012-01-30 14 views
7

Estoy tratando de optimizar un algoritmo de cálculo intensivo y estoy atascado en algún problema de caché. Tengo un gran buffer que se escribe ocasionalmente y al azar y se lee solo una vez al final de la aplicación. Obviamente, escribir en el búfer produce muchos errores de caché y además contamina las cachés que luego se necesitan nuevamente para el cálculo. Traté de utilizar instrinsics de movimiento no temporal, pero las fallas de caché (reportadas por valgrind y soportadas por mediciones de tiempo de ejecución) aún ocurren. Sin embargo, para seguir investigando movimientos no temporales, escribí un pequeño programa de prueba, que puedes ver a continuación. Acceso secuencial, gran buffer, solo escribe.¿Por qué _mm_stream_ps produce errores de caché L1/LL?

#include <stdio.h> 
#include <stdlib.h> 
#include <time.h> 
#include <smmintrin.h> 

void tim(const char *name, void (*func)()) { 
    struct timespec t1, t2; 
    clock_gettime(CLOCK_REALTIME, &t1); 
    func(); 
    clock_gettime(CLOCK_REALTIME, &t2); 
    printf("%s : %f s.\n", name, (t2.tv_sec - t1.tv_sec) + (float) (t2.tv_nsec - t1.tv_nsec)/1000000000); 
} 

const int CACHE_LINE = 64; 
const int FACTOR = 1024; 
float *arr; 
int length; 

void func1() { 
    for(int i = 0; i < length; i++) { 
     arr[i] = 5.0f; 
    } 
} 

void func2() { 
    for(int i = 0; i < length; i += 4) { 
     arr[i] = 5.0f; 
     arr[i+1] = 5.0f; 
     arr[i+2] = 5.0f; 
     arr[i+3] = 5.0f; 
    } 
} 

void func3() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 4) { 
     _mm_stream_ps(&arr[i], buf); 
    } 
} 

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[4], buf); 
     _mm_stream_ps(&arr[8], buf); 
     _mm_stream_ps(&arr[12], buf); 
    } 
} 

int main() { 
    length = CACHE_LINE * FACTOR * FACTOR; 

    arr = malloc(length * sizeof(float)); 
    tim("func1", func1); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func2", func2); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func3", func3); 
    free(arr); 

    arr = malloc(length * sizeof(float)); 
    tim("func4", func4); 
    free(arr); 

    return 0; 
} 

La función 1 es el enfoque ingenuo, la función 2 utiliza el desenrollado de bucle. La función 3 usa movntps, que de hecho se insertó en el ensamblaje al menos cuando revisé -O0. En la función 4 traté de emitir varias instrucciones movntps a la vez para ayudar a la CPU a hacer su combinación de escritura. Recopilé el código con gcc -g -lrt -std=gnu99 -OX -msse4.1 test.c donde X es uno de [0..3]. Los resultados son .. interesante que decir, en el mejor:

-O0 
func1 : 0.407794 s. 
func2 : 0.320891 s. 
func3 : 0.161100 s. 
func4 : 0.401755 s. 
-O1 
func1 : 0.194339 s. 
func2 : 0.182536 s. 
func3 : 0.101712 s. 
func4 : 0.383367 s. 
-O2 
func1 : 0.108488 s. 
func2 : 0.088826 s. 
func3 : 0.101377 s. 
func4 : 0.384106 s. 
-O3 
func1 : 0.078406 s. 
func2 : 0.084927 s. 
func3 : 0.102301 s. 
func4 : 0.383366 s. 

Como se puede ver _mm_stream_ps es un poco más rápido que los otros cuando el programa no está optimizado por gcc pero luego falla significativamente su propósito cuando optimización de gcc se enciende . Valgrind aún informa muchos errores de escritura en la memoria caché.

Por lo tanto, las preguntas son: ¿Por qué esas fallas de caché (L1 + LL) todavía ocurren incluso si estoy usando las instrucciones de transmisión de NTA? ¿Por qué especialmente func4 es tan lento? ¿Alguien puede explicar/especular qué está pasando aquí?

+2

Si está compilando con la optimización activada, usted tendrá que buscar en la asamblea para saber realmente lo que está sucediendo. – RussS

+0

Estoy mirando el conjunto, que por cierto es cada vez más difícil de leer con cada nivel de optimización, pero no me dice por qué se ignora la sugerencia no temporal. Al menos supongo que se ignora, ya que valgrind todavía informa caché falla donde no espero. De todos modos, sé que la pregunta es bastante inespecífica, por lo que realmente agradecería cualquier aporte sobre lo que pueda suceder aquí. –

Respuesta

8
  1. Probablemente, sus medidas de referencia de rendimiento sobre todo la asignación de memoria, no sólo el rendimiento de escritura. Su sistema operativo puede asignar páginas de memoria no en malloc, pero el primer toque, dentro de sus funciones func*. El sistema operativo también puede hacer algunos cambios aleatorios de memoria después de asignar gran cantidad de memoria, por lo que cualquier punto de referencia, realizado justo después de las asignaciones de memoria, puede no ser confiable.
  2. el código tiene aliasing problema: el compilador no puede garantizar que el puntero de la matriz no cambia en el proceso de llenar esta matriz, por lo que tiene que cargar siempre arr valor de la memoria en lugar de utilizar un registro. Esto puede costar una disminución en el rendimiento. La forma más fácil de evitar el aliasing es copiar arr y length en variables locales y usar solo variables locales para llenar el conjunto. Hay muchos consejos bien conocidos para evitar las variables globales. Aliasing es una de las razones.
  3. _mm_stream_ps funciona mejor si la matriz está alineada en 64 bytes. En su código, no se garantiza la alineación (en realidad, malloc lo alinea en 16 bytes). Esta optimización solo se nota para arreglos cortos.
  4. Es una buena idea llamar al _mm_mfence después de que haya terminado con _mm_stream_ps. Esto es necesario para la corrección, no para el rendimiento.
+1

Muchas gracias Evgeny! 1. Esto es todo. No estaba enterado de eso. Cuando cambié el código para asignar la memoria solo una vez, cambió drásticamente los tiempos de ejecución a lo que esperaba inicialmente. func3 + 4 son aproximadamente 2-3x más rápido que func1 + 2. 2. ¿Puede profundizar en esto un poco más? Pensé que el aliasing solo sería un problema relacionado con la memoria física <-> memoria física. No veo dónde esto es un problema aquí. 3. Ok, ¿entonces tendría que usar valloc() o alguna otra función específica de libc? No tuvo ningún impacto en los tiempos de ejecución. La alineación de la línea de caché debería ayudar a la CPU con la combinación de escritura, ¿estoy en lo cierto? 4. Ok. –

+1

Agregué algunas explicaciones sobre el aliasing así como el enlace de wikipedia. La alineación de la línea de caché permite utilizar correctamente la combinación de escritura para los primeros 64 bytes de la matriz. Para la alineación puede usar varias funciones dependientes de la plataforma, no las recuerdo todas ahora. O puede usar el truco '(p + 63) & ~ 63'. O simplemente ignore la alineación si sus matrices son siempre más grandes que megabytes. –

+1

Acerca del problema del aliasing, debe intentar pasar 'arr' y 'length' como argumentos a sus funciones, en lugar de tenerlos como globales. Esto * puede * mejorar las oportunidades de optimización para el compilador. – rotoglup

2

¿No debería ser esto func4:

void func4() { 
    __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f); 
    for(int i = 0; i < length; i += 16) { 
     _mm_stream_ps(&arr[i], buf); 
     _mm_stream_ps(&arr[i+4], buf); 
     _mm_stream_ps(&arr[i+8], buf); 
     _mm_stream_ps(&arr[i+12], buf); 
    } 
} 
+0

Tienes razón.Gracias :-) Esto lleva func4 a los mismos resultados que func3. –

Cuestiones relacionadas