Me encontré con un problema de rendimiento extraño en un punto de referencia de multiplicación de matriz (matrix_mult en Metis desde el paquete MOSBENCH). El punto de referencia se optimizó para ajustar los datos de forma tal que el conjunto de trabajo activo fuera de 12kb (3 mosaicos de 32x32 ints) y encajaría en la caché L1. Para resumir, el intercambio de los dos bucles más internos tuvo una diferencia de rendimiento de casi 4x en ciertos tamaños de entrada de matriz (4096, 8192) y alrededor de un 30% de diferencia en otros. El problema básicamente se reducía a acceder a los elementos secuencialmente versus en un patrón de zancada. Determinados tamaños de matriz creo que crearon un mal acceso de zancada que generó muchas colisiones en la línea de caché. La diferencia de rendimiento es notablemente menor cuando se cambia de L1 asociativo de 2 vías a L1 asociativo de 8 vías.optimización del bucle anidado del compilador para acceso a la memoria secuencial.
Mi pregunta es por qué gcc no optimiza el orden de bucles para maximizar los accesos de memoria secuenciales?
A continuación se muestra una versión simplificada del problema (tenga en cuenta que los tiempos de rendimiento dependen en gran medida de la configuración de L1. Los números que se muestran a continuación son del sistema 2.3 GHZ AMD con 64K L1 de 2 vías compilado con -O3).
N = ARRAY_SIZE // 1024
int* mat_A = (int*)malloc(N*N*sizeof(int));
int* mat_B = (int*)malloc(N*N*sizeof(int));
int* mat_C = (int*)malloc(N*N*sizeof(int));
// Elements of mat_B are accessed in a stride pattern of length N
// This takes 800 msec
for (int t = 0; t < 1000; t++)
for (int a = 0; a < 32; a++)
for (int b = 0; b < 32; b++)
for (int c = 0; c < 32; c++)
mat_C[N*a+b] += mat_A[N*a+c] * mat_B[N*c+b];
// Inner two loops are swapped
// Elements are now accessed sequentially in inner loop
// This takes 172 msec
for (int t = 0; t < 1000; t++)
for (int a = 0; a < 32; a++)
for (int c = 0; c < 32; c++)
for (int b = 0; b < 32; b++)
mat_C[N*a+b] += mat_A[N*a+c] * mat_B[N*c+b];
Porque es muy difícil probar para el compilador que tal cambio no cambia la semántica de la operación. – Sven
tal vez le resulte interesante google gcc + graphite, que era una rama ahora fusionada para transformaciones de bucle basadas en un modelo poliédrico. Hay un lugar en la lista de posibles transformaciones. –
Creo que dado que la suma entera es conmutativa, sería bastante simple para un compilador probar que la operación es invariable para ordenar el looper. Me pregunto qué hay sobre el ejemplo de código que hace que no sea trivial para optimizar. – Mark