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í?
Si está compilando con la optimización activada, usted tendrá que buscar en la asamblea para saber realmente lo que está sucediendo. – RussS
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í. –