¿Por qué el orden de los bucles afecta el rendimiento cuando se itera sobre una matriz 2D?


Posible Duplicado:
Cuál de estos dos bucles for es más eficiente en términos de tiempo y rendimiento de caché

A continuación hay dos programas que son casi idénticos, excepto que cambié las variables i y j. Ambos se ejecutan en diferentes cantidades de tiempo. ¿Alguien podría explicar por qué sucede esto?

Versión 1

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

main () {
  int i,j;
  static int x[4000][4000];
  for (i = 0; i < 4000; i++) {
    for (j = 0; j < 4000; j++) {
      x[j][i] = i + j; }
  }
}

Versión 2

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

main () {
  int i,j;
  static int x[4000][4000];
  for (j = 0; j < 4000; j++) {
     for (i = 0; i < 4000; i++) {
       x[j][i] = i + j; }
   }
}
Author: Community, 2012-03-30

7 answers

Como otros han dicho, el problema es el almacén de la ubicación de memoria en la matriz: x[i][j]. Aquí hay un poco de perspicacia por qué:

Tienes una matriz de 2 dimensiones, pero la memoria en la computadora es inherentemente de 1 dimensión. Así que mientras imaginas tu matriz como esta:

0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3

Su computadora lo almacena en la memoria como una sola línea:

0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3

En el 2do ejemplo, se accede a la matriz haciendo un bucle sobre el 2do número primero, es decir:

x[0][0] 
        x[0][1]
                x[0][2]
                        x[0][3]
                                x[1][0] etc...

Lo que significa que los estás golpeando a todos en orden. Ahora mira la 1ª versión. Estás haciendo:

x[0][0]
                                x[1][0]
                                                                x[2][0]
        x[0][1]
                                        x[1][1] etc...

Debido a la forma en que C presentó la matriz 2-d en la memoria, le estás pidiendo que salte por todas partes. Pero ahora para el pateador: ¿Por qué importa esto? Todos los accesos a la memoria son iguales, ¿verdad?

No: debido a cachés. Los datos de su memoria se traen a la CPU en pequeños trozos (llamados 'líneas de caché'), típicamente 64 bytes. Si tiene enteros de 4 bytes, eso significa que está obteniendo 16 enteros consecutivos en un poco ordenado paquete. En realidad, es bastante lento recuperar estos trozos de memoria; su CPU puede hacer mucho trabajo en el tiempo que tarda una sola línea de caché en cargarse.

Ahora mira hacia atrás en el orden de los accesos: El segundo ejemplo es (1) agarrar un trozo de 16 ints, (2) modificarlos todos, (3) repetir 4000*4000/16 veces. Eso es agradable y rápido, y la CPU siempre tiene algo en lo que trabajar.

El primer ejemplo es (1) tomar un trozo de 16 ints, (2) modificar solo uno de ellos, (3) repetir 4000 * 4000 tiempo. Eso va a requerir 16 veces el número de" recuperaciones " de la memoria. Su CPU realmente tendrá que pasar tiempo sentado esperando que aparezca esa memoria, y mientras está sentado, está perdiendo un tiempo valioso.

Nota Importante:

Ahora que tienes la respuesta, aquí hay una nota interesante: no hay ninguna razón inherente para que tu segundo ejemplo tenga que ser el rápido. Por ejemplo, en Fortran, el primer ejemplo sería rápido y el segundo lento. Esto se debe a que en lugar de expandir las cosas en "filas" conceptuales como lo hace C, Fortran se expande en "columnas", es decir:

0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3

La disposición de C se llama 'row-major' y la de Fortran se llama 'column-major'. Como puedes ver, es muy importante saber si tu lenguaje de programación es row-major o column-major. Aquí hay un enlace para más información: http://en.wikipedia.org/wiki/Row-major_order

 536
Author: Robert Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-07 15:28:52

Nada que ver con el montaje. Esto se debe a que la caché falla.

C Los arrays multidimensionales se almacenan con la última dimensión como la más rápida. Así que la primera versión perderá la caché en cada iteración, mientras que la segunda versión no lo hará.

Véase también: http://en.wikipedia.org/wiki/Loop_interchange .

 62
Author: Oliver Charlesworth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-30 02:20:03

La versión 2 se ejecutará mucho más rápido porque utiliza la caché de su computadora mejor que la versión 1. Si lo piensas, los arrays son solo áreas contiguas de la memoria. Cuando solicita un elemento en una matriz, su sistema operativo probablemente traerá una página de memoria a la caché que contiene ese elemento. Sin embargo, dado que los siguientes elementos también están en esa página (porque son contiguos), ¡el siguiente acceso ya estará en caché! Esto es lo que la versión 2 está haciendo para conseguir su velocidad.

Versión 1, por otro lado, está accediendo a los elementos en sentido de columna, y no en sentido de fila. Este tipo de acceso no es contiguo a nivel de memoria, por lo que el programa no puede aprovechar tanto el almacenamiento en caché del sistema operativo.

 22
Author: Oleksi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-30 02:21:45

El motivo es el acceso a datos locales en caché. En el segundo programa que está escaneando linealmente a través de la memoria que se beneficia de almacenamiento en caché y prefetching. El patrón de uso de memoria de su primer programa está mucho más extendido y, por lo tanto, tiene un peor comportamiento de caché.

 12
Author: Variable Length Coder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-30 02:22:38

Además de las otras excelentes respuestas en cache hits, también hay una posible diferencia de optimización. Es probable que el compilador optimice su segundo bucle en algo equivalente a:

  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }

Esto es menos probable para el primer bucle, porque necesitaría incrementar el puntero "p" con 4000 cada vez.

EDITAR: p++ e incluso *p++ = .. se puede compilar a una sola instrucción de CPU en la mayoría de las CPU. *p = ..; p += 4000 no se puede, por lo que hay menos beneficio en optimizarla. Es también más difícil, porque el compilador necesita saber y usar el tamaño de la matriz interna. Y no ocurre tan a menudo en el bucle interno en el código normal (ocurre solo para matrices multidimensionales, donde el último índice se mantiene constante en el bucle, y el penúltimo se escalona), por lo que la optimización es menos una prioridad.

 10
Author: fishinear,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-07 15:02:48

Esta línea el culpable :

x[j][i]=i+j;

La segunda versión utiliza memoria continua, por lo que será sustancialmente más rápido.

Lo intenté con

x[50000][50000];

Y el tiempo de ejecución es 13s para la versión 1 versus 0.6 s para la versión 2.

 8
Author: Nicolas Modrzyk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-30 02:29:24

Intento dar una respuesta genérica.

Porque i[y][x] es una abreviatura de *(i + y*array_width + x) en C (pruebe el clásico int P[3]; 0[P] = 0xBEEF;).

Al iterar sobre y, itera sobre trozos de tamaño array_width * sizeof(array_element). Si tienes eso en tu bucle interno, entonces tendrás array_width * array_height iteraciones sobre esos trozos.

Al voltear el orden, solo tendrá array_height iteraciones de fragmentos, y entre cualquier iteración de fragmentos, tendrá array_width iteraciones de solo sizeof(array_element).

Mientras que en really old x86-CPUs esto no importaba mucho, hoy en día ' x86 hacer una gran cantidad de prefetching y almacenamiento en caché de datos. Es probable que produzca muchos errores de caché en su orden de iteración más lento.

 3
Author: Sebastian Mach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-30 15:20:15