c# performance

c# - ¿Por qué esto es más rápido en 64 bits que en 32 bits?



performance (5)

He estado haciendo algunas pruebas de rendimiento, principalmente para poder entender la diferencia entre los iteradores y los bucles simples. Como parte de esto, creé un conjunto simple de pruebas y me sorprendieron totalmente los resultados. Para algunos métodos, 64 bits fue casi 10 veces más rápido que 32 bits.

Lo que busco es una explicación de por qué sucede esto.

[La siguiente respuesta indica que esto se debe a la aritmética de 64 bits en una aplicación de 32 bits. Cambiar los largos 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; }

Tengo un arnés de prueba simple que prueba esto

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 ejecuto estos, obtengo 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 el uso de LongLength es lento. Si utilizo array.Length, el rendimiento del primer bucle for es bastante bueno en 64 bits, pero no en 32 bits.

La otra cosa que leí de esto es que la iteración sobre una matriz es tan eficiente como un bucle for, y el código es mucho más limpio y fácil de leer.


Como dijeron otros, hacer aritmética de 64 bits en una máquina de 32 bits va a requerir una manipulación adicional, más aún si se hace la multiplicación o la división.

Volviendo a su preocupación acerca de los iteradores en comparación con los bucles simples, los iteradores pueden tener definiciones bastante complejas, y solo serán rápidas si la integración en línea y la optimización del compilador son capaces 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 de ensamblaje generado. Otra forma es ponerlo en un bucle de larga duración, pausarlo y mirar la pila para ver qué está haciendo.


El tipo de datos largo es de 64 bits y en un proceso de 64 bits, se procesa como una sola 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.


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 en gran medida variables long (enteros de 64 bits).

Por ejemplo, en el ensamblaje x64, para agregar un par de enteros de 64 bits almacenados en registros, simplemente puede hacer:

; adds rbx to rax add rax, rbx

Para realizar la misma operación en un procesador x86 de 32 bits, tendrá que usar dos registros y usar manualmente la operación 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, recuperación de memoria, ... lo que en última instancia dará como resultado un rendimiento reducido. La diferencia es muy notable en aplicaciones de procesamiento de números.

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

Con respecto a su punto sobre la iteración de matrices, el compilador de C # es lo suficientemente inteligente como para reconocer cada una de las matrices y tratarlas especialmente. El código generado es idéntico al uso de un bucle for y se recomienda que use foreach si no necesita cambiar el elemento de la 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 de límites para los accesos de matriz dentro del bucle. Esto no sucederá en el caso de LongLength y dará como resultado una disminución del rendimiento (tanto para el caso de 32 bits como para el caso de 64 bits); y como utilizará variables long con LongLength , el rendimiento de 32 bits se degradará aún más.


No estoy seguro de "por qué" pero me aseguraría de llamar a su "método" al menos una vez fuera de su ciclo de temporizador para que no esté contando jitting por primera vez. (Ya que esto me parece C #).


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 de resultado variable
  3. Una larga gama de resultados.

Así que necesitas tres variables. El acceso variable es más rápido si puede almacenarlos en registros; Si necesita moverlos dentro y fuera de la memoria, está perdiendo velocidad. Para largos 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.

Suma de números: la suma debe ser dos veces; la primera vez sin bit de acarreo y la segunda vez con bit de carry. 64 bits se puede hacer en un ciclo.

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

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