¿Por qué mi programa es lento cuando recorro exactamente 8192 elementos?


755

Aquí está el extracto del programa en cuestión. La matriz img[][]tiene el tamaño SIZE × SIZE, y se inicializa en:

img[j][i] = 2 * j + i

Luego, haces una matriz res[][], y cada campo aquí está hecho para ser el promedio de los 9 campos a su alrededor en la matriz img. El borde se deja en 0 por simplicidad.

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Eso es todo lo que hay en el programa. Para completar, esto es lo que viene antes. Ningún código viene después. Como puede ver, es solo inicialización.

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

Básicamente, este programa es lento cuando SIZE es un múltiplo de 2048, por ejemplo, los tiempos de ejecución:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

El compilador es GCC. Por lo que sé, esto se debe a la gestión de la memoria, pero realmente no sé demasiado sobre ese tema, por lo que pregunto aquí.

Además, cómo solucionar esto sería bueno, pero si alguien pudiera explicar estos tiempos de ejecución, ya estaría lo suficientemente feliz.

Ya conozco malloc / free, pero el problema no es la cantidad de memoria utilizada, es simplemente el tiempo de ejecución, por lo que no sé cómo ayudaría eso.


67
@bokan ocurre cuando el tamaño es un múltiplo de la zancada crítica del caché.
Luchian Grigore

55
@Mysticial, no importa, expone el mismo problema exacto; el código puede ser diferente, pero básicamente las dos preguntas se hacen al mismo tiempo (y sus títulos son definitivamente similares).
Griwes

33
No debe procesar la imagen con una matriz de 2 dimensiones si desea un alto rendimiento. Considere que todos los píxeles están en un formato sin procesar y procéselos como una matriz de una dimensión. Haz esto borroso en dos pasadas. Primero agregue el valor de los píxeles circundantes utilizando una suma deslizante de 3 píxeles: slideSum + = src [i + 1] -src [i-1]; dest [i] = slideSum ;. Luego haga lo mismo verticalmente y divida al mismo tiempo: dest [i] = (src [i-width] + src [i] + src [i + width]) / 9. www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan el

8
En realidad hay dos cosas pasando aquí. No es solo una superalineación.
Mysticial

77
(Solo un pequeño detalle en su respuesta. Para el primer segmento de código, sería bueno si todos sus bucles for tuvieran llaves).
Trevor Boyd Smith,

Respuestas:


954

La diferencia es causada por el mismo problema de superalineación de las siguientes preguntas relacionadas:

Pero eso es solo porque hay otro problema con el código.

A partir del bucle original:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Primero note que los dos bucles internos son triviales. Se pueden desenrollar de la siguiente manera:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Eso deja los dos bucles externos que nos interesan.

Ahora podemos ver que el problema es el mismo en esta pregunta: ¿Por qué el orden de los bucles afecta el rendimiento al iterar sobre una matriz 2D?

Está iterando la matriz a nivel de columna en lugar de a nivel de fila.


Para resolver este problema, debe intercambiar los dos bucles.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Esto elimina por completo todo el acceso no secuencial, por lo que ya no obtienes ralentizaciones aleatorias en grandes potencias de dos.


Core i7 920 @ 3.5 GHz

Código original:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Bucles exteriores intercambiados:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
También notaré que desenrollar los bucles internos no tiene ningún efecto en el rendimiento. El compilador probablemente lo hace automáticamente. Los desenrollé con el único propósito de deshacerme de ellos para que sea más fácil detectar el problema con los bucles externos.
Mysticial

29
Y puede acelerar este código en otro factor de tres almacenando en caché las sumas a lo largo de cada fila. Pero esa y otras optimizaciones están fuera del alcance de la pregunta original.
Eric Postpischil

34
@ClickUpvote Esto es realmente un problema de hardware (almacenamiento en caché). No tiene nada que ver con el idioma. Si lo intentó en cualquier otro idioma que compila o JIT a código nativo, probablemente verá los mismos efectos.
Mysticial

19
@ClickUpvote: Pareces bastante equivocado. Ese "segundo bucle" era simplemente místico desenrollando los bucles internos a mano. Esto es algo que su compilador seguramente hará de todos modos, y Mystical solo lo hizo para que el problema con los bucles externos sea más obvio. De ninguna manera es algo que deba molestarse en hacer usted mismo.
Lily Ballard

154
ESTE es un ejemplo perfecto de una buena respuesta en SO: hace referencia a preguntas similares, explica paso a paso cómo lo abordó, explica el problema, explica cómo CORREGIR el problema, tiene un formato excelente e incluso un ejemplo del código en ejecución en tu máquina Gracias por tu contribución.
MattSayar

57

Las siguientes pruebas se han realizado con el compilador de Visual C ++, ya que es utilizado por la instalación predeterminada de Qt Creator (supongo que sin bandera de optimización). Cuando uso GCC, no hay una gran diferencia entre la versión de Mystical y mi código "optimizado". Entonces, la conclusión es que las optimizaciones del compilador se encargan de la micro optimización mejor que los humanos (yo por fin). Dejo el resto de mi respuesta como referencia.


No es eficiente procesar imágenes de esta manera. Es mejor usar matrices de dimensión única. El procesamiento de todos los píxeles se realiza en un bucle. El acceso aleatorio a los puntos se puede hacer usando:

pointer + (x + y*width)*(sizeOfOnePixel)

En este caso particular, es mejor calcular y almacenar en caché la suma de tres grupos de píxeles horizontalmente porque se usan tres veces cada uno.

He hecho algunas pruebas y creo que vale la pena compartirlas. Cada resultado es un promedio de cinco pruebas.

Código original por usuario1615209:

8193: 4392 ms
8192: 9570 ms

Versión mística:

8193: 2393 ms
8192: 2190 ms

Dos pases utilizando una matriz 1D: primer pase para sumas horizontales, segundo para suma vertical y promedio. Direccionamiento de dos pasos con tres punteros y solo incrementos como este:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

Dos pasadas usando una matriz 1D y direccionando así:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Una pasada almacenando sumas horizontales solo una fila por delante para que permanezcan en la caché:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Conclusión:

  • No hay beneficios de usar varios punteros y solo incrementos (pensé que habría sido más rápido)
  • El almacenamiento en caché de sumas horizontales es mejor que calcularlas varias veces.
  • Dos pases no son tres veces más rápidos, solo dos veces.
  • Es posible lograr 3,6 veces más rápido utilizando una sola pasada y almacenando en caché un resultado intermedio

Estoy seguro de que es posible hacerlo mucho mejor.

NOTA Tenga en cuenta que escribí esta respuesta para abordar problemas generales de rendimiento en lugar del problema de caché explicado en la excelente respuesta de Mystical. Al principio era solo un pseudocódigo. Me pidieron que hiciera pruebas en los comentarios ... Aquí hay una versión completamente refactorizada con pruebas.


99
"Creo que es al menos 3 veces más rápido", ¿le gustaría respaldar esa afirmación con algunas métricas o citas?
Adam Rosenfield

8
@AdamRosenfield "Creo" = suposición! = "Es" = reclamo. No tengo métrica para esto y me gustaría ver una prueba. Pero el mío requiere 7 incrementos, 2 sub, 2 sumar y un div por píxel. Cada bucle usa menos var local que el que hay registrado en la CPU. El otro requiere 7 incrementos, 6 decrementos, 1 div y entre 10 y 20 mul para el direccionamiento dependiendo de la optimización del compilador. Además, cada instrucción en el bucle requiere el resultado de la instrucción anterior, esto descarta los beneficios de la arquitectura súper escalar de Pentiums. Entonces tiene que ser más rápido.
bokan

3
La respuesta a la pregunta original tiene que ver con los efectos de memoria y caché. La razón por la que el código de OP es tan lento es que su patrón de acceso a la memoria va por columnas en lugar de por filas, lo que tiene una localidad de referencia de caché muy pobre. Es particularmente malo en 8192 porque luego las filas consecutivas terminan usando las mismas líneas de caché en un caché de mapeo directo o caché con baja asociatividad, por lo que la tasa de pérdida de caché es aún mayor. Intercambiar los bucles proporciona un enorme aumento de rendimiento al aumentar considerablemente la localidad de caché.
Adam Rosenfield

1
Bien hecho, esos son algunos números impresionantes. Como descubrió, se trata del rendimiento de la memoria: el uso de varios punteros con incrementos no proporcionó ningún beneficio.
Adam Rosenfield

2
@ AdamRosenfield Estaba muy preocupado esta mañana porque no pude reproducir las pruebas. Parece que el aumento de rendimiento es solo con el compilador de Visual C ++. Usando gcc, solo hay una pequeña diferencia.
bokan
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.