visual studio rendimiento rapido muy mejorar mas lento inicio hacer compilar como code administrar acelerar c# performance 32bit-64bit

c# - rapido - mejorar el rendimiento de visual studio 2017



Gran diferencia de rendimiento(26 veces más rápido) al compilar para 32 y 64 bits (4)

Estaba tratando de medir la diferencia de usar un for y un foreach al acceder a listas de tipos de valores y tipos de referencia.

Usé la siguiente clase para hacer el perfil.

public static class Benchmarker { public static void Profile(string description, int iterations, Action func) { Console.Write(description); // Warm up func(); Stopwatch watch = new Stopwatch(); // Clean up GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); watch.Start(); for (int i = 0; i < iterations; i++) { func(); } watch.Stop(); Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations); } }

Usé double para mi tipo de valor. Y creé esta ''clase falsa'' para probar los tipos de referencia:

class DoubleWrapper { public double Value { get; set; } public DoubleWrapper(double value) { Value = value; } }

Finalmente ejecuté este código y comparé las diferencias de tiempo.

static void Main(string[] args) { int size = 1000000; int iterationCount = 100; var valueList = new List<double>(size); for (int i = 0; i < size; i++) valueList.Add(i); var refList = new List<DoubleWrapper>(size); for (int i = 0; i < size; i++) refList.Add(new DoubleWrapper(i)); double dummy; Benchmarker.Profile("valueList for: ", iterationCount, () => { double result = 0; for (int i = 0; i < valueList.Count; i++) { unchecked { var temp = valueList[i]; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); Benchmarker.Profile("valueList foreach: ", iterationCount, () => { double result = 0; foreach (var v in valueList) { var temp = v; result *= temp; result += temp; result /= temp; result -= temp; } dummy = result; }); Benchmarker.Profile("refList for: ", iterationCount, () => { double result = 0; for (int i = 0; i < refList.Count; i++) { unchecked { var temp = refList[i].Value; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); Benchmarker.Profile("refList foreach: ", iterationCount, () => { double result = 0; foreach (var v in refList) { unchecked { var temp = v.Value; result *= temp; result += temp; result /= temp; result -= temp; } } dummy = result; }); SafeExit(); }

Seleccioné las opciones Release y Any CPU , ejecuté el programa y obtuve los siguientes horarios:

valueList for: average time: 483,967938 ms valueList foreach: average time: 477,873079 ms refList for: average time: 490,524197 ms refList foreach: average time: 485,659557 ms Done!

Luego seleccioné las opciones Release y x64, ejecuté el programa y obtuve los siguientes horarios:

valueList for: average time: 16,720209 ms valueList foreach: average time: 15,953483 ms refList for: average time: 19,381077 ms refList foreach: average time: 18,636781 ms Done!

¿Por qué la versión x64 bit es mucho más rápida? Esperaba alguna diferencia, pero no algo tan grande.

No tengo acceso a otras computadoras. ¿Podría por favor ejecutar esto en sus máquinas y decirme los resultados? Estoy usando Visual Studio 2015 y tengo un Intel Core i7 930.

Aquí está el método SafeExit() , para que pueda compilar / ejecutar usted mismo:

private static void SafeExit() { Console.WriteLine("Done!"); Console.ReadLine(); System.Environment.Exit(1); }

Según lo solicitado, utilizando double? en lugar de mi DoubleWrapper :

Cualquier CPU

valueList for: average time: 482,98116 ms valueList foreach: average time: 478,837701 ms refList for: average time: 491,075915 ms refList foreach: average time: 483,206072 ms Done!

x64

valueList for: average time: 16,393947 ms valueList foreach: average time: 15,87007 ms refList for: average time: 18,267736 ms refList foreach: average time: 16,496038 ms Done!

Por último, pero no menos importante: crear un perfil x86 me da casi los mismos resultados de usar Any CPU .


Puede haber varias razones por las cuales esto se ejecuta más rápido en 64 bits en su máquina. La razón por la que le pregunté qué CPU estaba usando era porque cuando las CPU de 64 bits aparecieron por primera vez, AMD e Intel tenían diferentes mecanismos para manejar el código de 64 bits.

Arquitectura del procesador:

La arquitectura de la CPU de Intel era puramente de 64 bits. Para ejecutar el código de 32 bits, las instrucciones de 32 bits debían convertirse (dentro de la CPU) en instrucciones de 64 bits antes de la ejecución.

La arquitectura de CPU de AMD fue construir 64 bits justo encima de su arquitectura de 32 bits; es decir, era esencialmente una arquitectura de 32 bits con extensiones de 64 bits, no había proceso de conversión de código.

Obviamente, esto fue hace unos años, así que no tengo idea de si / cómo ha cambiado la tecnología, pero esencialmente, esperaría que el código de 64 bits funcione mejor en una máquina de 64 bits ya que la CPU puede trabajar con el doble de la cantidad de bits por instrucción.

.NET JIT

Se argumenta que .NET (y otros lenguajes administrados como Java) son capaces de superar a los lenguajes como C ++ debido a la forma en que el compilador JIT puede optimizar su código de acuerdo con la arquitectura de su procesador. A este respecto, es posible que el compilador JIT esté utilizando algo en la arquitectura de 64 bits que posiblemente no estaba disponible o requirió una solución alternativa cuando se ejecutó en 32 bits.

Nota:

En lugar de usar DoubleWrapper, ¿ha considerado usar Nullable<double> o sintaxis abreviada: double? - Me interesaría ver si eso tiene algún impacto en sus pruebas.

Nota 2: Algunas personas parecen estar combinando mis comentarios sobre la arquitectura de 64 bits con IA-64. Solo para aclarar, en mi respuesta, 64 bits se refiere a x86-64 y 32 bits se refiere a x86-32. ¡Aquí nada hace referencia a IA-64!


Puedo reproducir esto en 4.5.2. No RyuJIT aquí. Los desmontajes x86 y x64 parecen razonables. Las comprobaciones de rango, etc., son las mismas. La misma estructura básica. No se desenrolla el bucle.

x86 usa un conjunto diferente de instrucciones flotantes. El rendimiento de estas instrucciones parece ser comparable con las instrucciones x64, excepto por la división :

  1. Las instrucciones flotantes x87 de 32 bits utilizan una precisión de 10 bytes internamente.
  2. La división de precisión extendida es súper lenta.

La operación de división hace que la versión de 32 bits sea extremadamente lenta. Descomentar la división iguala el rendimiento en gran medida (32 bits por debajo de 430 ms a 3,25 ms).

Peter Cordes señala que las latencias de instrucción de las dos unidades de coma flotante no son tan diferentes. Quizás algunos de los resultados intermedios son números desnormalizados o NaN. Estos pueden desencadenar un camino lento en una de las unidades. O tal vez los valores divergen entre las dos implementaciones debido a la precisión de flotación de 10 bytes frente a 8 bytes.

Peter Cordes también señala que todos los resultados intermedios son NaN ... Al eliminar este problema ( valueList.Add(i + 1) para que ningún divisor sea cero), la mayoría de los resultados se igualan. Aparentemente, al código de 32 bits no le gustan los operandos de NaN. Imprimamos algunos valores intermedios: if (i % 1000 == 0) Console.WriteLine(result); . Esto confirma que los datos ahora son sanos.

Al realizar una evaluación comparativa, debe comparar una carga de trabajo realista. ¿Pero quién hubiera pensado que una división inocente puede arruinar su punto de referencia?

Intente simplemente sumar los números para obtener un mejor punto de referencia.

La división y el módulo son siempre muy lentos. Si modifica el código del Dictionary BCL para simplemente no usar el operador de módulo para calcular el rendimiento del índice de depósito, se pueden mejorar las mejoras mensurables. Así de lenta es la división.

Aquí está el código de 32 bits:

Código de 64 bits (misma estructura, división rápida):

Esto no está vectorizado a pesar de las instrucciones SSE que se utilizan.


Tenemos la observación de que el 99.9% de todas las operaciones de punto flotante involucrarán NaN, lo cual es al menos altamente inusual (encontrado por Peter Cordes primero). Tenemos otro experimento por usr, que descubrió que eliminar las instrucciones de división hace que la diferencia horaria desaparezca casi por completo.

Sin embargo, el hecho es que los NaN solo se generan porque la primera división calcula 0.0 / 0.0 que da el NaN inicial. Si no se realizan las divisiones, el resultado siempre será 0.0, y siempre calcularemos 0.0 * temp -> 0.0, 0.0 + temp -> temp, temp - temp = 0.0. Entonces, eliminar la división no solo eliminó las divisiones, sino que también eliminó los NaN. Esperaría que los NaN sean realmente el problema, y ​​que una implementación maneje los NaN muy lentamente, mientras que la otra no tiene el problema.

Valdría la pena comenzar el ciclo en i = 1 y medir nuevamente. Las cuatro operaciones resultan * temp, + temp, / temp, - temp efectivamente suman (1 - temp) para que no tengamos números inusuales (0, infinito, NaN) para la mayoría de las operaciones.

El único problema podría ser que la división siempre da un resultado entero, y algunas implementaciones de división tienen atajos cuando el resultado correcto no usa muchos bits. Por ejemplo, dividir 310.0 / 31.0 da 10.0 como los primeros cuatro bits con un resto de 0.0, y algunas implementaciones pueden dejar de evaluar los 50 bits restantes, mientras que otros no. Si hay una diferencia significativa, entonces comenzar el ciclo con resultado = 1.0 / 3.0 marcaría la diferencia.


valueList[i] = i , comenzando desde i=0 , por lo que la primera iteración del bucle es 0.0 / 0.0 . Por lo tanto, cada operación en todo su punto de referencia se realiza con NaN s.

Como mostró @usr en la salida de desensamblaje , la versión de 32 bits usaba punto flotante x87, mientras que 64 bits usaba punto flotante SSE.

No soy un experto en rendimiento con NaN s, o la diferencia entre x87 y SSE para esto, pero creo que esto explica la diferencia de rendimiento 26x. Apuesto a que sus resultados estarán mucho más cerca entre 32 y 64 bits si inicializa valueList[i] = i+1 . (actualización: usr confirmó que esto hizo que el rendimiento de 32 y 64 bits fuera bastante cercano).

La división es muy lenta en comparación con otras operaciones. Vea mis comentarios sobre la respuesta de @ usr. Consulte también agner.org/optimize para obtener toneladas de excelentes cosas sobre hardware y la optimización de asm y C / C ++, algunas de ellas relevantes para C #. Tiene tablas de instrucciones de latencia y rendimiento para la mayoría de las instrucciones para todas las CPU x86 recientes.

Sin embargo, 10B x87 fdiv no es mucho más lento que el divsd doble precisión 8B de SSE2, para valores normales. IDK sobre las diferencias de rendimiento con NaN, infinitos o denormales.

Sin embargo, tienen diferentes controles para lo que sucede con NaN y otras excepciones de FPU. La palabra de control x87 FPU es independiente del registro de control de excepción / redondeo SSE (MXCSR). Si x87 obtiene una excepción de CPU para cada división, pero SSE no, eso explica fácilmente el factor de 26. O tal vez solo hay una diferencia de rendimiento tan grande cuando se manejan NaNs. El hardware no está optimizado para moverse a través de NaN después de NaN .

IDK si el SSE controla para evitar ralentizaciones con denormales entrará en juego aquí, ya que creo que el result será NaN todo el tiempo. IDK si C # establece el indicador denormals-are-zero en el MXCSR, o el indicador flush-to-zero (que escribe ceros en primer lugar, en lugar de tratar los denormals como cero cuando se lee de nuevo).

Encontré un artículo de Intel sobre controles de coma flotante SSE, que lo contrasta con la palabra de control x87 FPU. Sin embargo, no tiene mucho que decir sobre NaN . Termina con esto:

Conclusión

Para evitar problemas de serialización y rendimiento debido a los números denormales y de subflujo, use las instrucciones SSE y SSE2 para configurar los modos Flush-to-Zero y Denormals-Are-Zero dentro del hardware para permitir el mayor rendimiento para aplicaciones de punto flotante.

IDK si esto ayuda con la división por cero.

para vs foreach

Puede ser interesante probar un cuerpo de bucle que tiene un rendimiento limitado, en lugar de ser una sola cadena de dependencia transportada por un bucle. Tal como están las cosas, todo el trabajo depende de resultados anteriores; no hay nada que la CPU pueda hacer en paralelo (aparte de los límites: verifique la siguiente carga de la matriz mientras se está ejecutando la cadena mul / div).

Es posible que vea más diferencia entre los métodos si el "trabajo real" ocupara más recursos de ejecución de las CPU. Además, en Intel anterior a Sandybridge, hay una gran diferencia entre un ajuste de bucle en el búfer de bucle 28uop o no. Obtiene cuellos de botella de decodificación de instrucciones si no, esp. cuando la duración promedio de la instrucción es mayor (lo que sucede con SSE). Las instrucciones que decodifican a más de una uop también limitarán el rendimiento del decodificador, a menos que vengan en un patrón que sea agradable para los decodificadores (por ejemplo, 2-1-1). Por lo tanto, un bucle con más instrucciones de sobrecarga de bucle puede marcar la diferencia entre un ajuste de bucle en la caché uop de 28 entradas o no, lo cual es un gran problema en Nehalem y, a veces, es útil en Sandybridge y más adelante.