¿Qué tan lentas son las excepciones de .NET?

No quiero una discusión sobre cuándo y no arrojar excepciones. Deseo resolver un problema simple. El 99% del tiempo, el argumento para no lanzar excepciones gira alrededor de que sean lentas, mientras que el otro lado afirma (con una prueba comparativa) que la velocidad no es el problema. He leído numerosos blogs, artículos y publicaciones pertenecientes a uno u otro lado. Entonces, ¿cuál es?

Algunos enlaces de las respuestas: Skeet , Mariani , Brumme .

Estoy en el lado “no lento” – o más precisamente “no lo suficientemente lento como para que valga la pena evitarlos en el uso normal”. He escrito dos breves artículos sobre esto. Hay críticas sobre el aspecto del punto de referencia, que en su mayoría se debe a que “en la vida real habría más stack por pasar, por lo que soplarás el caché, etc.”, pero usar códigos de error para subir la stack también volar el caché, así que no veo eso como un argumento particularmente bueno.

Solo para dejarlo en claro: no apoyo el uso de excepciones donde no son lógicas. Por ejemplo, int.TryParse es completamente apropiado para convertir datos de un usuario. Es apropiado cuando lee un archivo generado por la máquina, donde el error significa “El archivo no está en el formato que debe ser, realmente no quiero tratar de manejar esto ya que no sé qué más podría estar mal. ”

Cuando uso excepciones en “solo circunstancias razonables”, nunca he visto una aplicación cuyo rendimiento se vio significativamente afectado por las excepciones. Básicamente, las excepciones no deberían ocurrir a menudo a menos que tenga importantes problemas de corrección, y si tiene importantes problemas de corrección, entonces el rendimiento no es el mayor problema al que se enfrenta.

Existe la respuesta definitiva a esto del tipo que los implementó: Chris Brumme. Escribió un excelente artículo de blog sobre el tema (advertencia: es muy largo) (warning2) está muy bien escrito, si eres un experto en tecnología lo leerás hasta el final y luego tendrás que recuperar tus horas después del trabajo 🙂 )

El resumen ejecutivo: son lentos Se implementan como excepciones Win32 SEH, por lo que algunos incluso pasarán el límite del anillo 0 CPU. Obviamente, en el mundo real, harás muchas otras tareas para que la extraña excepción no se note en absoluto, pero si las usas para el flujo del progtwig, excepto para que se aplaste tu aplicación. Este es otro ejemplo de la máquina de comercialización de MS que nos hace un flaco favor. Recuerdo a un microsoftie que nos contó cómo incurrieron absolutamente en gastos generales, lo cual es completo.

Chris da una cita pertinente:

De hecho, el CLR usa internamente excepciones incluso en las partes no administradas del motor. Sin embargo, existe un grave problema de rendimiento a largo plazo con las excepciones y esto debe tenerse en cuenta en su decisión.

No tengo idea de qué están hablando las personas cuando dicen que son lentas solo si son arrojadas.

EDITAR: si no se lanzan las excepciones, significa que está haciendo una nueva excepción () o algo así. De lo contrario, la excepción hará que se suspenda el hilo y que se recorra la stack. Esto puede ser aceptable en situaciones más pequeñas, pero en sitios web de alto tráfico, depender de excepciones como un flujo de trabajo o un mecanismo de ruta de ejecución ciertamente le causará problemas de rendimiento. Las excepciones, per se, no son malas, y son útiles para express condiciones excepcionales

El flujo de trabajo de excepción en una aplicación .NET utiliza excepciones de primera y segunda oportunidad. Para todas las excepciones, incluso si las está capturando y manipulándolas, el objeto de excepción aún se crea y el marco de trabajo aún tiene que recorrer la stack para buscar un controlador. Si atrapa y vuelve a lanzar, por supuesto, eso va a llevar más tiempo: obtendrá una excepción de primera oportunidad, la atrapará, la volverá a lanzar, causará otra excepción de primera oportunidad, que luego no encontrará un controlador, lo que a su vez causará una excepción de segunda oportunidad.

Las excepciones también son objetos en el montón, por lo que si lanza toneladas de excepciones, está causando problemas de rendimiento y de memoria.

Además, de acuerdo con mi copia de “Performance Testing Microsoft .NET Web Applications” escrita por el equipo de ACE:

“La gestión de excepciones es costosa. La ejecución del hilo involucrado se suspende mientras CLR recurre a través de la stack de llamadas en busca del manejador de excepciones correcto, y cuando se encuentra, el manejador de excepciones y algunos bloques finalmente deben tener la oportunidad de ejecutarse antes de que se pueda realizar un procesamiento regular “.

Mi propia experiencia en el campo mostró que la reducción de excepciones ayudó significativamente al rendimiento. Por supuesto, hay otras cosas que se tienen en cuenta cuando se realizan pruebas de rendimiento: por ejemplo, si se graba una E / S de disco o si las consultas se realizan en segundos, ese debería ser su objective. Pero encontrar y eliminar excepciones debería ser una parte vital de esa estrategia.

El argumento, tal como lo entiendo, no es que arrojar excepciones es malo, son lentas per se. En cambio, se trata de utilizar el constructo throw / catch como una forma de primera clase de controlar la lógica de aplicación normal, en lugar de construcciones condicionales más tradicionales.

A menudo, en la lógica de aplicación normal, se realiza un bucle en el que se repite la misma acción miles / millones de veces. En este caso, con algunos perfiles muy simples (consulte la clase Cronómetro), puede ver por sí mismo que lanzar una excepción en lugar de decir una simple statement if puede ser sustancialmente más lenta.

De hecho, una vez leí que el equipo .NET de Microsoft introdujo los métodos TryXXXXX en .NET 2.0 para muchos de los tipos FCL básicos, específicamente porque los clientes se quejaban de que el rendimiento de sus aplicaciones era muy lento.

En muchos casos, esto se debió a que los clientes estaban intentando la conversión de valores de tipo en un bucle, y cada bash falló. Se lanzó una excepción de conversión y luego fue capturada por un controlador de excepción que luego se tragó la excepción y continuó el ciclo.

Microsoft ahora recomienda que los métodos TryXXX se utilicen particularmente en esta situación para evitar posibles problemas de rendimiento.

Podría estar equivocado, pero parece que no está seguro acerca de la veracidad de los “puntos de referencia” sobre los que ha leído. Solución simple: Pruébelo usted mismo.

Mi servidor XMPP obtuvo un aumento de velocidad importante (lo siento, no hay números reales, puramente observacionales) después de que intenté evitarlos constantemente (como comprobar si un socket está conectado antes de tratar de leer más datos) y darme formas de evitarlos (los métodos TryX mencionados). Eso fue con solo unos 50 usuarios virtuales (en chat) activos.

Nunca he tenido ningún problema de rendimiento con excepciones. Utilizo muchas excepciones: nunca uso códigos de retorno si puedo. Son una mala práctica, y en mi opinión, huelen a código de spaghetti.

Creo que todo se reduce a cómo usas las excepciones: si las usas como códigos de retorno (cada llamada al método en la stack atrapa y vuelve a lanzar) entonces, sí, serán lentas, porque tienes sobrecarga cada captura / lanzamiento.

Pero si tira al pie de la stack y atrapa en la parte superior (sustituye una cadena entera de códigos de retorno con un lanzamiento / atrapada), todas las operaciones costosas se realizan una vez.

Al final del día, son una característica de idioma válida.

Solo para demostrar mi punto

Ejecute el código en este enlace (demasiado grande para una respuesta).

Resultados en mi computadora:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

Las marcas de tiempo se envían al principio, entre códigos de retorno y excepciones, al final. Toma el mismo tiempo en ambos casos. Tenga en cuenta que debe comstackr con optimizaciones.

Si los comparas con los códigos de retorno, son lentos como el infierno. Sin embargo, como los carteles anteriores indicaban que no querías utilizar el progtwig normal, solo recibes el golpe de perforación cuando ocurre un problema y en la gran mayoría de los casos ya no importa el rendimiento (ya que la excepción implica un locking de ruta).

Definitivamente vale la pena utilizar códigos de error, las ventajas son enormes IMO.

Solo para agregar mi propia experiencia reciente a esta discusión: de acuerdo con la mayoría de lo que está escrito arriba, encontré que arrojar excepciones era extremadamente lento cuando se hacía de forma repetida, incluso sin ejecutar el depurador. Acabo de boost un 60% el rendimiento de un gran progtwig que estoy escribiendo al cambiar unas cinco líneas de código: cambiar a un modelo de código de retorno en lugar de lanzar excepciones. Por supuesto, el código en cuestión se estaba ejecutando miles de veces y potencialmente arrojando miles de excepciones antes de que lo cambiara. Así que estoy de acuerdo con la afirmación anterior: lanzar excepciones cuando algo importante realmente sale mal, no como una forma de controlar el flujo de aplicaciones en cualquier situación “esperada”.

Pero Mono lanza la excepción 10 veces más rápido que el modo autónomo de .net, y el modo autónomo de .net arroja la excepción 60 veces más rápido que el modo depurador .net. (Las máquinas de prueba tienen el mismo modelo de CPU)

 int c = 1000000; int s = Environment.TickCount; for (int i = 0; i < c; i++) { try { throw new Exception(); } catch { } } int d = Environment.TickCount - s; Console.WriteLine(d + "ms / " + c + " exceptions"); 

En el modo de lanzamiento, la sobrecarga es mínima.

A menos que vaya a utilizar excepciones para el control de flujo (por ejemplo, salidas no locales) de forma recursiva, dudo que pueda notar la diferencia.

En el CLR de Windows, para una cadena de llamadas de profundidad 8, lanzar una excepción es 750 veces más lento que comprobar y propagar un valor de retorno. (ver a continuación para los puntos de referencia)

Este alto costo para las excepciones se debe a que el CLR de Windows se integra con algo llamado Manejo de excepciones estructuradas de Windows . Esto permite que las excepciones se capturen adecuadamente y se publiquen en diferentes tiempos de ejecución e idiomas. Sin embargo, es muy, muy lento.

Las excepciones en el tiempo de ejecución de Mono (en cualquier plataforma) son mucho más rápidas, porque no se integra con SEH. Sin embargo, hay pérdida de funcionalidad al pasar excepciones en múltiples tiempos de ejecución porque no utiliza nada como SEH.

Aquí hay resultados abreviados de mi punto de referencia de excepciones vs valores de retorno para el CLR de Windows.

 baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208 ms exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639 ms exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms 

Y aquí está el código …

 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { public class TestIt { int value; public class TestException : Exception { } public int getValue() { return value; } public void reset() { value = 0; } public bool baseline_null(bool shouldfail, int recurse_depth) { if (recurse_depth <= 0) { return shouldfail; } else { return baseline_null(shouldfail,recurse_depth-1); } } public bool retval_error(bool shouldfail, int recurse_depth) { if (recurse_depth <= 0) { if (shouldfail) { return false; } else { return true; } } else { bool nested_error = retval_error(shouldfail,recurse_depth-1); if (nested_error) { return true; } else { return false; } } } public void exception_error(bool shouldfail, int recurse_depth) { if (recurse_depth <= 0) { if (shouldfail) { throw new TestException(); } } else { exception_error(shouldfail,recurse_depth-1); } } public static void Main(String[] args) { int i; long l; TestIt t = new TestIt(); int failures; int ITERATION_COUNT = 1000000; // (0) baseline null workload for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; t.baseline_null(shoulderror,recurse_depth); } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } // (1) retval_error for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; if (!t.retval_error(shoulderror,recurse_depth)) { failures++; } } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } // (2) exception_error for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) { for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) { int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq); failures = 0; DateTime start_time = DateTime.Now; t.reset(); for (i = 1; i < ITERATION_COUNT; i++) { bool shoulderror = (i % EXCEPTION_MOD) == 0; try { t.exception_error(shoulderror,recurse_depth); } catch (TestException e) { failures++; } } double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds; Console.WriteLine( String.Format( "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms", recurse_depth, exception_freq, failures,elapsed_time)); } } } } } 

Una nota rápida aquí sobre el rendimiento asociado con la captura de excepciones.

Cuando la ruta de ejecución ingresa en un bloque de ‘prueba’, no ocurre nada mágico. No hay instrucciones de “prueba” ni costo asociado con entrar o salir del bloque de prueba. La información sobre el bloque try se almacena en los metadatos del método, y estos metadatos se utilizan en tiempo de ejecución cada vez que se produce una excepción. El motor de ejecución recorre la stack buscando la primera llamada que estaba contenida en un bloque de prueba. Cualquier sobrecarga asociada con el manejo de excepciones ocurre solo cuando se lanzan excepciones.

Cuando escribir clases / funciones para que otros lo usen parece ser difícil de decir cuando las excepciones son apropiadas. Hay algunas partes útiles de BCL que tuve que abandonar e ir por pinvoke porque arrojan excepciones en lugar de devolver errores. En algunos casos, puede solucionarlo, pero para otros, como System.Management y Performance Counters, existen usos en los que debe realizar bucles en los que BCL genera excepciones con frecuencia.

Si está escribiendo una biblioteca y existe una posibilidad remota de que su función se pueda utilizar en un bucle y exista la posibilidad de una gran cantidad de iteraciones, use el patrón Try … o de alguna otra forma para exponer los errores junto a las excepciones. Y aún así, es difícil decir cuánto se llamará a su función si está siendo utilizada por muchos procesos en un entorno compartido.

En mi propio código, las excepciones solo se lanzan cuando las cosas son tan excepcionales que es necesario mirar el seguimiento de la stack y ver qué falló y luego arreglarlo. Así que prácticamente he vuelto a escribir partes de BCL para usar el manejo de errores basado en el patrón Try .. en lugar de excepciones.