2009-12-21 19 views
18

He estado haciendo algunas pruebas de rendimiento, principalmente para poder entender la diferencia entre iteradores y bucles simples. Como parte de esto, creé un conjunto simple de pruebas y luego me sorprendieron por completo los resultados. Para algunos métodos, 64 bits era casi 10 veces más rápido que 32 bits.¿Por qué es esto más rápido en 64 bit que en 32 bit?

Lo que estoy buscando es una explicación de por qué esto está sucediendo.

[La respuesta a continuación indica que esto se debe a la aritmética de 64 bits en una aplicación de 32 bits. El cambio de longs a ints da como resultado un buen rendimiento en sistemas de 32 y 64 bits.]

Aquí están los 3 métodos en cuestión.

private static long ForSumArray(long[] array) 
{ 
    var result = 0L; 
    for (var i = 0L; i < array.LongLength; i++) 
    { 
     result += array[i]; 
    } 
    return result; 
} 

private static long ForSumArray2(long[] array) 
{ 
    var length = array.LongLength; 
    var result = 0L; 
    for (var i = 0L; i < length; i++) 
    { 
     result += array[i]; 
    } 
    return result; 
} 

private static long IterSumArray(long[] array) 
{ 
    var result = 0L; 
    foreach (var entry in array) 
    { 
     result += entry; 
    } 
    return result; 
} 

que tienen un simple instrumento de prueba que pone a prueba esta

var repeat = 10000; 

var arrayLength = 100000; 
var array = new long[arrayLength]; 
for (var i = 0; i < arrayLength; i++) 
{ 
    array[i] = i; 
} 

Console.WriteLine("For: {0}", AverageRunTime(repeat,() => ForSumArray(array))); 

repeat = 100000; 
Console.WriteLine("For2: {0}", AverageRunTime(repeat,() => ForSumArray2(array))); 
Console.WriteLine("Iter: {0}", AverageRunTime(repeat,() => IterSumArray(array))); 

private static TimeSpan AverageRunTime(int count, Action method) 
{ 
    var stopwatch = new Stopwatch(); 
    stopwatch.Start(); 
    for (var i = 0; i < count; i++) 
    { 
     method(); 
    } 
    stopwatch.Stop(); 
    var average = stopwatch.Elapsed.Ticks/count; 
    return new TimeSpan(average); 
} 

Cuando corro éstos, consigo los siguientes resultados:
32 bits:

For: 00:00:00.0006080 
For2: 00:00:00.0005694 
Iter: 00:00:00.0001717

64 bits

For: 00:00:00.0007421 
For2: 00:00:00.0000814 
Iter: 00:00:00.0000818

Lo que leo de esto es que usar LongLength es lento. Si uso array.Length, el rendimiento para el primer ciclo for es bastante bueno en 64 bits, pero no en 32 bits.

La otra cosa que leí de esto es que iterar en una matriz es tan eficiente como un ciclo for, ¡y el código es mucho más limpio y fácil de leer!

+0

Lo que también me parece interesante es que, obviamente, el compilador JIT no optimiza el acceso array.LongLength. – newgre

Respuesta

50

Los procesadores x64 contienen registros de propósito general de 64 bits con los que pueden calcular operaciones en enteros de 64 bits en una sola instrucción. Los procesadores de 32 bits no tienen eso. Esto es especialmente relevante para su programa, ya que utiliza mucho las variables long (entero de 64 bits).

Por ejemplo, en el montaje x64, añadir un par de 64 bits enteros almacenados en los registros, sólo tiene que hacer:

; adds rbx to rax 
add rax, rbx 

Para hacer la misma operación en un procesador x86 de 32 bits, tendrá utilizar dos registros y utilizar manualmente el arrastre de la primera operación en la segunda operación:

; adds ecx:ebx to edx:eax 
add eax, ebx 
adc edx, ecx 

Más instrucciones y menos registros significan más ciclos de reloj, recupera la memoria, ... que en última instancia se traducirá en un menor rendimiento. La diferencia es muy notable en las aplicaciones de procesamiento numérico.

Para aplicaciones .NET, parece que el compilador JIT de 64 bits realiza optimizaciones más agresivas que mejoran el rendimiento general.

En cuanto a su punto acerca de la iteración de matriz, el compilador de C# es lo suficientemente inteligente como para reconocer foreach sobre las matrices y tratarlas especialmente. El código generado es idéntico al uso de un bucle for y se recomienda utilizar foreach si no necesita cambiar el elemento de matriz en el bucle.Además de eso, el tiempo de ejecución reconoce el patrón for (int i = 0; i < a.Length; ++i) y omite las comprobaciones encuadernadas de los accesos a la matriz dentro del ciclo. Esto no sucederá en el caso LongLength y dará como resultado una disminución del rendimiento (tanto para el caso de 32 bits como de 64 bits); y dado que usará las variables long con LongLength, el rendimiento de 32 bits se degradará aún más.

+4

El número de registros también se incrementa en los procesadores x64, pero no usan esos registros cuando ejecutan código de 32 bits, solo código de 64 bits. – Powerlord

+0

¡Gran comentario sobre el compilador C# y foreach, especialmente los límites que comprueban los accesos a la matriz! –

1

No estoy seguro de "por qué", pero me aseguraría de llamar a su "método" al menos una vez fuera de su bucle de temporizador para que no cuente la primera vez que hace jits. (Dado que esto se parece a C# para mí).

5

El tipo de datos largo es de 64 bits y en un proceso de 64 bits, se procesa como una única unidad de longitud nativa. En un proceso de 32 bits, se trata como 2 unidades de 32 bits. Las matemáticas, especialmente en estos tipos "divididos" serán intensivas en el procesador.

1

Oh, eso es fácil. Supongo que está utilizando la tecnología x86. ¿Qué necesitas para hacer los bucles en ensamblador?

  1. Una variable de índice i
  2. Un resultado variable resultado
  3. Una larga variedad de resultados.

Así que necesita tres variables. El acceso variable es más rápido si puede almacenarlos en registros; si necesita moverlos hacia adentro y hacia afuera en la memoria, está perdiendo velocidad. Para longitudes de 64 bits necesita dos registros en 32 bits y solo tenemos cuatro registros, por lo que es muy probable que todas las variables no puedan almacenarse en registros, sino que deben almacenarse en un almacenamiento intermedio como la pila. Esto solo ralentizará el acceso considerablemente.

Adición de números: La suma debe ser dos veces; la primera vez sin acarreo y la segunda vez con acarreo. 64 bits puede hacerlo en un ciclo.

Movimiento/carga: Por cada 1 ciclo de 64 bits var, necesita dos ciclos para 32 bits para cargar/descargar un entero largo en la memoria.

Cada tipo de datos componente (tipos de datos que consta de más bits que los bits de registro/dirección) perderá una velocidad considerable. Las ganancias de velocidad de un orden de magnitud es la razón por la que las GPU aún prefieren flotadores (32 bits) en lugar de dobles (64 bits).

0

Como han dicho otros, hacer aritmética de 64 bits en una máquina de 32 bits requerirá más manipulación, más, si se multiplica o se divide.

Volviendo a su preocupación acerca de los iteradores frente a los bucles simples, los iteradores pueden tener definiciones bastante complejas, y solo serán rápidos si la optimización y la optimización del compilador es capaz de reemplazarlos con la forma simple equivalente. Realmente depende del tipo de iterador y la implementación del contenedor subyacente. La forma más sencilla de saber si se ha optimizado razonablemente bien es examinar el código ensamblador generado. Otra forma es ponerlo en un bucle de larga duración, pausarlo y mirar la pila para ver qué está haciendo.

Cuestiones relacionadas