Matrices rectangulares .NET: ¿cómo acceder en un bucle?
Pregunta
Básicamente tienes dos formas de hacer esto:
for (int x = 0; x < UPPER_X; x++)
for (int y = 0; y < UPPER_Y; y++)
{
arr1[x, y] = get_value();
arr2[y, x] = get_value();
}
La única diferencia es qué variable cambiar en el bucle interno: primero o segundo. Escuché que los resultados difieren de un idioma a otro.
¿Cuál es el orden correcto para .NET?
Solución
El motivo por el cual uno es más rápido que el otro tiene que ver con la memoria caché del procesador y la forma en que los datos se almacenan en la memoria.
Hay dos formas normales de almacenar los datos bidimensionales en un espacio de direcciones unidimensional. Puede almacenar todos los datos para la primera fila, luego la segunda fila, y así sucesivamente (también conocido como orden principal de fila ), o puede hacerlo por columnas (también conocido como < a href = "http://en.wikipedia.org/wiki/Row-major_order#Column-major_order" rel = "nofollow noreferrer"> orden principal de columna ). A continuación se muestran las ubicaciones de memoria para una matriz de 3x3 para ambas opciones.
Filas:
1 2 3
4 5 6
7 8 9
Columnas:
1 4 7
2 5 8
3 6 9
Cuando accede a una ubicación de memoria, se carga una línea de caché completa (que puede tener entre 8 y 512 bytes, según Wikipedia). Entonces, si accede a la siguiente ubicación de memoria, ya estará en el caché. Por lo tanto, puede ser mucho más rápido acceder a la memoria secuencialmente que saltar en el espacio de direcciones. Por lo tanto, con grandes matrices bidimensionales, puede haber una diferencia de velocidad significativa entre elegir filas o columnas como su variable de bucle interno.
Otros consejos
Debes comparar tus circunstancias particulares para estar seguro.
Pensaría que no habría diferencia para las matrices rectangulares (es decir, memoria asignada contiguamente), PERO de acuerdo con esto Artículo de MSDN hay una diferencia:
Puede obtener mejores resultados si convertir una matriz multidimensional en una matriz unidimensional. Si no te importa la sintaxis, esto puede ser trivial; solo use un índice como compensar. Por ejemplo, lo siguiente declara una matriz unidimensional para ser utilizado como una matriz bidimensional:
double[] myArray = new double[ROW_DIM * COLUMN_DIM];
Para indexar los elementos de este matriz, utilice el siguiente desplazamiento:
myArray[row * COLUMN_DIM + column];
Esto, sin duda, será más rápido que un equivalente dentado o rectangular matriz.
Así que hice el punto de referencia y los resultados son que el acceso a arr1 es tres veces más rápido.
Muy interesante, nunca me detuve a considerar que puede ser una gran diferencia simplemente accediendo a los índices de la matriz "no secuencialmente". Lo intenté yo mismo, y también encontré que el siguiente código tenía el segundo bucle entre 2 y 3 veces más largo:
// Hmmm, how to insert blank lines in the code formatter???
static void Main(string[] args)
{
Stopwatch timer = new Stopwatch();
int arraySize = 10000;
// First array, access X by Y
int[,] xbyy = new int[arraySize, arraySize];
timer.Start();
for (int x = 0; x < arraySize; x++)
for (int y = 0; y < arraySize; y++)
{
xbyy[x, y] = 15;
}
timer.Stop();
TimeSpan duration = timer.Elapsed;
string realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds",
duration.Minutes, duration.Seconds, duration.Milliseconds);
Console.WriteLine("X by Y took " + realTimeFormat);
// Seecond array, access Y by X
int[,] ybyx = new int[arraySize, arraySize];
timer.Start();
for (int x = 0; x < arraySize; x++)
for (int y = 0; y < arraySize; y++)
{
ybyx[y, x] = 15;
}
timer.Stop();
duration = timer.Elapsed;
realTimeFormat = string.Format("{0:00} minutes {1:00} seconds {2:000} milliseconds",
duration.Minutes, duration.Seconds, duration.Milliseconds);
Console.WriteLine("Y by X took " + realTimeFormat);
Console.ReadLine();
}
Para mantener las cosas cortas, aquí están los fragmentos de IL emitidos para el bucle X por Y y el bucle Y por X.
Código inicial antes del bucle,
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 290 (0x122)
.maxstack 4
.locals init ([0] class [System]System.Diagnostics.Stopwatch timer,
[1] int32 arraySize,
[2] int32[0...,0...] xbyy,
[3] int32 x,
[4] int32 y,
[5] valuetype [mscorlib]System.TimeSpan duration,
[6] string realTimeFormat,
[7] int32[0...,0...] ybyx,
[8] int32 V_8,
[9] int32 V_9)
IL_0000: newobj instance void [System]System.Diagnostics.Stopwatch::.ctor()
IL_0005: stloc.0
IL_0006: ldc.i4 0x2710
IL_000b: stloc.1
bucle X por Y
IL_000c: ldloc.1
IL_000d: ldloc.1
IL_000e: newobj instance void int32[0...,0...]::.ctor(int32,
int32)
IL_0013: stloc.2
IL_0014: ldloc.0
IL_0015: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_001a: ldc.i4.0
IL_001b: stloc.3
IL_001c: br.s IL_003d
IL_001e: ldc.i4.0
IL_001f: stloc.s y
IL_0021: br.s IL_0034
IL_0023: ldloc.2
IL_0024: ldloc.3
IL_0025: ldloc.s y
IL_0027: ldc.i4.s 15
IL_0029: call instance void int32[0...,0...]::Set(int32,
int32,
int32)
IL_002e: ldloc.s y
IL_0030: ldc.i4.1
IL_0031: add
IL_0032: stloc.s y
IL_0034: ldloc.s y
IL_0036: ldloc.1
IL_0037: blt.s IL_0023
IL_0039: ldloc.3
IL_003a: ldc.i4.1
IL_003b: add
IL_003c: stloc.3
IL_003d: ldloc.3
IL_003e: ldloc.1
IL_003f: blt.s IL_001e
IL_0041: ldloc.0
bucle Y por X
IL_0090: ldloc.1
IL_0091: ldloc.1
IL_0092: newobj instance void int32[0...,0...]::.ctor(int32,
int32)
IL_0097: stloc.s ybyx
IL_0099: ldloc.0
IL_009a: callvirt instance void [System]System.Diagnostics.Stopwatch::Start()
IL_009f: ldc.i4.0
IL_00a0: stloc.s V_8
IL_00a2: br.s IL_00c7
IL_00a4: ldc.i4.0
IL_00a5: stloc.s V_9
IL_00a7: br.s IL_00bc
IL_00a9: ldloc.s ybyx
IL_00ab: ldloc.s V_9
IL_00ad: ldloc.s V_8
IL_00af: ldc.i4.s 15
IL_00b1: call instance void int32[0...,0...]::Set(int32,
int32,
int32)
IL_00b6: ldloc.s V_9
IL_00b8: ldc.i4.1
IL_00b9: add
IL_00ba: stloc.s V_9
IL_00bc: ldloc.s V_9
IL_00be: ldloc.1
IL_00bf: blt.s IL_00a9
IL_00c1: ldloc.s V_8
IL_00c3: ldc.i4.1
IL_00c4: add
IL_00c5: stloc.s V_8
IL_00c7: ldloc.s V_8
IL_00c9: ldloc.1
IL_00ca: blt.s IL_00a4
IL_00cc: ldloc.0
El flujo lógico IL es algo similar. La principal diferencia que puedo observar es que el primer ciclo logra usar stloc e ldloc para las posiciones 2 y 3 para la primera matriz y la variable de índice X. En el momento en que llegó al segundo bucle, había gastado el maxstack y, por lo tanto, utilizaba las instrucciones stloc.s y ldloc.s. Creo que esta es la diferencia entre hacer referencia a variables en la pila y en el montón, y contribuir al rendimiento más lento.
Ahora, si cambia el orden en que se prueban los bucles, de modo que el bucle Y por X se ejecute primero para acceder a las referencias de la pila, verá que las duraciones de tiempo se invierten.
ACTUALIZACIÓN: Me equivoqué al hacer referencia a direcciones de pila o montón. Parece que las primeras cuatro variables en un método son más eficientes para hacer referencia con los códigos de operación dedicados stloc.0, 1, 3, 4 y ldloc.0, 1, 3, 4.
http://weblogs.asp.net/mnolton /archive/2004/01/09/48992.aspx