una - try catch c# como usar
¿Cuál es la sobrecarga real de try/catch en C#? (12)
¿Está preguntando sobre la sobrecarga de usar try / catch / finally cuando no se lanzan las excepciones, o la sobrecarga de usar excepciones para controlar el flujo del proceso? Esto último es similar a usar una varilla de dinamita para encender la vela de cumpleaños de un niño pequeño, y la sobrecarga asociada cae en las siguientes áreas:
- Puede esperar errores de caché adicionales debido a la excepción lanzada que acceda a los datos residentes que normalmente no están en la memoria caché.
Puede esperar fallas de página adicionales debido a la excepción lanzada que acceda al código no residente y datos que normalmente no están en el conjunto de trabajo de su aplicación.
- por ejemplo, lanzar la excepción requerirá que CLR encuentre la ubicación de los bloques finally y catch basados en la IP actual y la IP de retorno de cada cuadro hasta que se maneje la excepción más el bloque de filtro.
- costo de construcción adicional y resolución de nombre para crear los marcos con fines de diagnóstico, incluida la lectura de metadatos, etc.
ambos elementos anteriores suelen acceder a datos y códigos "fríos", por lo que es probable que se produzcan fallas de página difíciles si tiene alguna presión de memoria:
- CLR intenta poner código y datos que se utilizan con poca frecuencia lejos de los datos que se utilizan con frecuencia para mejorar la localidad, por lo que esto funciona en contra de usted porque está forzando a que el frío esté caliente.
- el costo de las fallas de página rígida, si las hay, empequeñecerá todo lo demás.
- Las situaciones de captura típicas a menudo son profundas, por lo tanto, los efectos anteriores tienden a magnificarse (aumentando la probabilidad de errores de página).
En cuanto al impacto real del costo, esto puede variar mucho dependiendo de qué más esté sucediendo en su código en ese momento. Jon Skeet tiene un buen resumen aquí , con algunos enlaces útiles. Tiendo a estar de acuerdo con su afirmación de que si llega al punto en que las excepciones perjudican significativamente su desempeño, tiene problemas en términos de su uso de excepciones más allá del solo rendimiento.
Entonces, sé que try / catch agrega algo de sobrecarga y, por lo tanto, no es una buena forma de controlar el flujo del proceso, pero ¿de dónde viene esta sobrecarga y cuál es su impacto real?
Analicemos uno de los mayores costos posibles de un bloque try / catch cuando se utiliza donde no debería ser utilizado:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
Y aquí está el que no tiene try / catch:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
Sin contar el insignificante espacio en blanco, uno podría notar que estas dos piezas equivalentes de código tienen casi exactamente la misma longitud en bytes. Este último contiene 4 bytes menos de sangría. ¿Eso es algo malo?
Para agregar insulto a la lesión, un estudiante decide hacer un bucle mientras que la entrada se puede analizar como un int. La solución sin try / catch podría ser algo como:
while (int.TryParse(...))
{
...
}
¿Pero cómo se ve esto al usar try / catch?
try {
for (;;)
{
x = int.Parse(...);
...
}
}
catch
{
...
}
Los bloques de prueba / captura son formas mágicas de perder sangría, y ¡ni siquiera sabemos la razón por la que falló! Imagine cómo se siente la persona que realiza la depuración, cuando el código continúa ejecutándose después de una falla lógica grave, en lugar de detenerse con un error obvio de excepción. Los bloques Try / catch son una validación / saneamiento de datos de un perezoso.
Uno de los costos menores es que los bloques try / catch desactivan ciertas optimizaciones: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Creo que ese es un punto positivo también. Se puede usar para deshabilitar optimizaciones que de otro modo podrían paralizar los algoritmos de paso de mensajes seguros y sanos para aplicaciones multiproceso, y para detectar posibles condiciones de carrera;) Esa es la única situación en la que se me ocurre usar try / catch. Incluso eso tiene alternativas.
Contrariamente a las teorías comúnmente aceptadas, try
/ catch
puede tener implicaciones significativas de rendimiento, ¡y eso es si se lanza una excepción o no!
- Deshabilita algunas optimizaciones automáticas (por diseño) y, en algunos casos, inyecta código de depuración , como se puede esperar de un asistente de depuración . Siempre habrá personas que no estén de acuerdo conmigo en este punto, pero el lenguaje lo requiere y el desmontaje lo muestra, por lo que esas personas son delusional por definición del diccionario.
- Puede tener un impacto negativo en el mantenimiento. Este es en realidad el tema más importante aquí, pero dado que se eliminó mi última respuesta (que se centró casi exclusivamente en ella), trataré de centrarme en el problema menos importante (la microoptimización) en lugar del problema más importante ( la macro-optimización).
El primero ha sido cubierto en un par de publicaciones de blog por los MVP de Microsoft a lo largo de los años, y confío en que pueda encontrarlos fácilmente. se preocupa mucho por el contenido, así que proporcionaré enlaces a algunos de ellos como evidencia de relleno :
- Las implicaciones de rendimiento de
try
/catch
/finally
( y parte dos ), de Peter Ritchie explora las optimizaciones quetry
/catch
/finally
inhabilita (y profundizaré en esto con citas del estándar) - Performance Profiling
Parse
vs.TryParse
vs.ConvertTo
por Ian Huff afirma descaradamente que "el manejo de excepciones es muy lento" y demuestra este punto al enfrentar aInt.Parse
eInt.TryParse
uno contra el otro ... A cualquiera que insista en queTryParse
usetry
/catch
detrás de las escenas, esto debería arrojar algo de luz!
También hay esta respuesta que muestra la diferencia entre el código desmontado con y sin usar try
/ catch
.
Parece tan obvio que hay una sobrecarga que es evidentemente observable en la generación de código, y esa sobrecarga incluso parece ser reconocida por las personas que valoran Microsoft. Sin embargo, estoy repitiendo Internet ...
Sí, hay docenas de instrucciones MSIL adicionales para una línea trivial de código, y eso ni siquiera cubre las optimizaciones desactivadas, por lo que técnicamente es una micro-optimización.
Publiqué una respuesta hace años que se borró porque se centraba en la productividad de los programadores (la macro-optimización).
Esto es desafortunado ya que ningún ahorro de unos pocos nanosegundos aquí y allá del tiempo de CPU puede compensar muchas horas acumuladas de optimización manual por parte de humanos. ¿Para qué paga más tu jefe: una hora de tu tiempo o una hora con la computadora en funcionamiento? ¿En qué punto desconectamos el enchufe y admitimos que es hora de comprar una computadora más rápida ?
Claramente, deberíamos optimizar nuestras prioridades , ¡no solo nuestro código! En mi última respuesta, aproveché las diferencias entre dos fragmentos de código.
Usando try
/ catch
:
int x;
try {
x = int.Parse("1234");
}
catch {
return;
}
// some more code here...
No está usando try
/ catch
:
int x;
if (int.TryParse("1234", out x) == false) {
return;
}
// some more code here
Considere desde la perspectiva de un desarrollador de mantenimiento, que es más probable que pierda su tiempo, si no en la creación de perfiles / optimización (cubierto anteriormente) que probablemente ni siquiera sería necesario si no fuera por el problema de try
/ catch
, entonces en desplazándose por el código fuente ... ¡Uno de esos tiene cuatro líneas adicionales de basura repetitiva!
A medida que se introducen más y más campos en una clase, toda esta basura repetitiva se acumula (tanto en código fuente como en código desensamblado) mucho más allá de niveles razonables. Cuatro líneas adicionales por campo, y siempre son las mismas líneas ... ¿No nos enseñaron a evitar repetirnos? Supongo que podríamos ocultar el try
/ catch
detrás de una abstracción Int.TryParse
casa, pero ... entonces también podríamos evitar excepciones (es decir, usar Int.TryParse
).
Esto ni siquiera es un ejemplo complejo; He visto intentos de instanciar nuevas clases en try
/ catch
. Considere que todo el código dentro del constructor podría descalificarse de ciertas optimizaciones que de otra manera serían aplicadas automáticamente por el compilador. ¿Qué mejor manera de dar lugar a la teoría de que el compilador es lento , a diferencia del compilador que está haciendo exactamente lo que se le dice que haga ?
Suponiendo que el constructor arroje una excepción y, como resultado, se desencadena algún error, el desarrollador de mantenimiento deficiente tendrá que rastrearlo. Esa puede no ser una tarea tan fácil, ya que a diferencia del código de spaghetti de la pesadilla goto , try
/ catch
puede causar problemas en tres dimensiones , ya que podría subir la pila no solo en otras partes del mismo método, sino también en otras clases y métodos, todos los cuales serán observados por el desarrollador de mantenimiento, ¡de la manera más difícil ! Sin embargo, nos dicen que "ir es peligroso", ¡eh!
Al final menciono, try
/ catch
tiene su beneficio, es decir, ¡ está diseñado para deshabilitar optimizaciones ! Es, si se quiere, una ayuda de depuración . Para eso fue diseñado y es lo que debería usarse como ...
Creo que ese es un punto positivo también. Se puede usar para deshabilitar optimizaciones que de otro modo podrían paralizar los algoritmos de paso de mensajes seguros y sanos para aplicaciones multiproceso, y para detectar posibles condiciones de carrera;) Esa es la única situación en la que se me ocurre usar try / catch. Incluso eso tiene alternativas.
¿Qué optimizaciones try
, catch
y finally
desactivan?
AKA
¿Cómo se try
, catch
y finally
útiles como ayudas para la depuración?
son barreras de escritura. Esto viene del estándar:
12.3.3.13 declaraciones Try-catch
Para una declaración de estado de la forma:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n
- El estado de asignación definido de v al principio de try-block es el mismo que el estado de asignación definida de v al comienzo de stmt .
- El estado de asignación definido de v al comienzo de catch-block-i (para cualquier i ) es el mismo que el estado de asignación definida de v al comienzo de stmt .
- El estado de asignación definido de v en el punto final de stmt se asigna definitivamente si (y solo si) v se asigna definitivamente en el punto final de try-block y cada catch-block-i (para cada i de 1 a n )
En otras palabras, al comienzo de cada declaración try
:
- todas las asignaciones hechas a los objetos visibles antes de ingresar a la instrucción
try
deben estar completas, lo que requiere un bloqueo de hilo para el inicio, ¡lo que lo hace útil para depurar las condiciones de carrera! - el compilador no tiene permitido:
- eliminar asignaciones de variables no utilizadas que definitivamente se han asignado antes de la instrucción
try
- reorganice o combine cualquiera de sus asignaciones internas (es decir, consulte mi primer enlace, si aún no lo ha hecho).
- izar las asignaciones por encima de esta barrera, retrasar la asignación a una variable que sabe que no se usará hasta más tarde (si es que lo hace) o mover anticipadamente las asignaciones posteriores para hacer otras optimizaciones posibles ...
- eliminar asignaciones de variables no utilizadas que definitivamente se han asignado antes de la instrucción
Una historia similar se mantiene para cada declaración de catch
; supongamos que dentro de su declaración try
(o un constructor o función que invoque, etc.) que asigne a la variable que de otra manera no tiene sentido (digamos, garbage=42;
), el compilador no puede eliminar esa declaración, sin importar cuán irrelevante sea para el comportamiento observable del programa. La tarea debe completarse antes de que se ingrese el bloque catch
.
Por lo que vale, finally
cuenta una historia igualmente degradante :
12.3.3.14 declaraciones Try-finally
Para una declaración de prueba stmt de la forma:
try try-block finally finally-block
• El estado de asignación definida de v al principio de try-block es el mismo que el estado de asignación definida de v al comienzo de stmt .
• El estado de asignación definido de v al comienzo de finally-block es el mismo que el estado de asignación definida de v al comienzo de stmt .
• El estado de asignación definido de v en el punto final de stmt se asigna definitivamente si (y solo si): o v se asigna definitivamente en el punto final de try-block o v se asigna definitivamente en el punto final de finally-block Si se realiza una transferencia de flujo de control (como una declaración goto ) que comienza dentro de try-block y termina fuera de try-block , v también se considera definitivamente asignada en esa transferencia de flujo de control si v se asigna definitivamente a el punto final de finally-block . (Esto no es solo un if-if v definitivamente se asigna por otra razón en esta transferencia de flujo de control, entonces todavía se considera definitivamente asignado.)
12.3.3.15 declaraciones Try-catch-finally
Análisis de asignación definido para una declaración try - catch - finally del formulario:
try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block
se hace como si la declaración fuera una declaración try - finally que incluye una instrucción try - catch :
try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block
En mi experiencia, la mayor sobrecarga es lanzar una excepción y manejarla. Una vez trabajé en un proyecto en el que se usaba un código similar al siguiente para verificar si alguien tenía derecho a editar algún objeto. Este método HasRight () se usó en todas partes en la capa de presentación, y se solía llamar a cientos de objetos.
bool HasRight(string rightName, DomainObject obj) {
try {
CheckRight(rightName, obj);
return true;
}
catch (Exception ex) {
return false;
}
}
void CheckRight(string rightName, DomainObject obj) {
if (!_user.Rights.Contains(rightName))
throw new Exception();
}
Cuando la base de datos de pruebas se completó con datos de prueba, esto condujo a una desaceleración muy visible al abrir nuevos formularios, etc.
Así que lo refactoreé a lo siguiente, que según las últimas mediciones rápidas y sucias es aproximadamente 2 órdenes de magnitud más rápido:
bool HasRight(string rightName, DomainObject obj) {
return _user.Rights.Contains(rightName);
}
void CheckRight(string rightName, DomainObject obj) {
if (!HasRight(rightName, obj))
throw new Exception();
}
En resumen, usar excepciones en el flujo de proceso normal es aproximadamente dos órdenes de magnitud más lento que usar un flujo de proceso similar sin excepciones.
Es mucho más fácil escribir, depurar y mantener código que está libre de mensajes de error de compilación, mensajes de advertencia de análisis de código y excepciones aceptadas de rutina (particularmente excepciones que se lanzan en un lugar y se aceptan en otro). Debido a que es más fácil, el código, en promedio, estará mejor escrito y menos errores.
Para mí, ese programador y la sobrecarga de calidad es el argumento principal contra el uso de try-catch para el flujo del proceso.
La sobrecarga de excepciones de la computadora es insignificante en comparación, y generalmente pequeña en términos de la capacidad de la aplicación para cumplir con los requisitos de rendimiento del mundo real.
Hace un tiempo escribí un artículo sobre esto porque había mucha gente preguntando sobre esto en ese momento. Puede encontrarlo y el código de prueba en http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .
El resultado es que hay una pequeña cantidad de sobrecarga para un bloque try / catch pero es tan pequeña que debe ignorarse. Sin embargo, si está ejecutando bloques try / catch en bucles que se ejecutan millones de veces, es posible que desee considerar mover el bloque al exterior del bucle, si es posible.
El problema clave de rendimiento con los bloques try / catch es cuando realmente atrapas una excepción. Esto puede agregar un retraso notable a su aplicación. Por supuesto, cuando las cosas van mal, la mayoría de los desarrolladores (y muchos usuarios) reconocen la pausa como una excepción que está a punto de suceder. La clave aquí es no usar manejo de excepciones para operaciones normales. Como su nombre indica, son excepcionales y debes hacer todo lo posible para evitar que sean lanzados. No debe usarlos como parte del flujo esperado de un programa que está funcionando correctamente.
Hice una entrada en el blog sobre este tema el año pasado. Echale un vistazo. La conclusión es que casi no hay costo para un bloque de prueba si no se produce ninguna excepción, y en mi computadora portátil, una excepción fue de alrededor de 36 μs. Eso podría ser menos de lo que esperabas, pero ten en cuenta que esos resultados están en una pila poco profunda. Además, las primeras excepciones son realmente lentas.
Me gusta mucho la publicación de blog de Hafthor, y para agregar mi granito de arena a esta discusión, me gustaría decir que siempre ha sido fácil para mí tener la CAPA DE DATOS lanzar solo un tipo de excepción (DataAccessException). De esta forma, mi BUSINESS LAYER sabe qué excepción esperar y atrapa. Luego, dependiendo de otras reglas comerciales (es decir, si mi objeto comercial participa en el flujo de trabajo, etc.), puedo lanzar una nueva excepción (BusinessObjectException) o continuar sin re / throwing.
¡Diría que no dudes en utilizar try..catch cuando sea necesario y utilizarlo con prudencia!
Por ejemplo, este método participa en un flujo de trabajo ...
¿Comentarios?
public bool DeleteGallery(int id)
{
try
{
using (var transaction = new DbTransactionManager())
{
try
{
transaction.BeginTransaction();
_galleryRepository.DeleteGallery(id, transaction);
_galleryRepository.DeletePictures(id, transaction);
FileManager.DeleteAll(id);
transaction.Commit();
}
catch (DataAccessException ex)
{
Logger.Log(ex);
transaction.Rollback();
throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
}
}
}
catch (DbTransactionException ex)
{
Logger.Log(ex);
throw new BusinessObjectException("Cannot delete gallery.", ex);
}
return true;
}
No soy un experto en implementaciones de lenguaje (así que tome esto con un grano de sal), pero creo que uno de los mayores costos es deshacer la pila y almacenarla para el seguimiento de la pila. Sospecho que esto sucede solo cuando se lanza la excepción (pero no sé), y si es así, este sería un costo oculto decente cada vez que se lanza una excepción ... por lo que no es como si estuvieras saltando de un lugar en el código a otro, están sucediendo muchas cosas.
No creo que sea un problema, siempre y cuando utilice excepciones para el comportamiento EXCEPCIONAL (por lo que no es el camino típico y esperado a través del programa).
Podemos leer en Programming Languages Pragmatics de Michael L. Scott que los compiladores de hoy en día no agregan ninguna sobrecarga en el caso común, es decir, cuando no se producen excepciones. Entonces, cada trabajo se hace en tiempo de compilación. Pero cuando se lanza una excepción en el tiempo de ejecución, el compilador necesita realizar una búsqueda binaria para encontrar la excepción correcta, y esto sucederá con cada nuevo lanzamiento que realice.
Pero las excepciones son excepciones y este costo es perfectamente aceptable. Si intenta realizar el Manejo de excepciones sin excepciones y utiliza códigos de error de retorno, probablemente necesitará una declaración if para cada subrutina y esto generará una sobrecarga en tiempo real. Usted sabe que una instrucción if se convierte en unas pocas instrucciones de ensamblaje, que se realizarán cada vez que ingrese en sus sub-rutinas.
Perdón por mi inglés, espero que te ayude. Esta información se basa en el libro citado, para obtener más información, consulte el Capítulo 8.5 Manejo de excepciones.
Sin mencionar que si está dentro de un método frecuentemente llamado, puede afectar el comportamiento general de la aplicación.
Por ejemplo, considero el uso de Int32.Parse como una mala práctica en la mayoría de los casos, ya que arroja excepciones para algo que, de otro modo, puede captarse fácilmente.
Entonces, para concluir todo lo escrito aquí:
1) Use los bloques try ... catch para detectar errores inesperados, casi sin penalización de rendimiento.
2) No use excepciones para errores exceptuados si puede evitarlo.
Tres puntos para hacer aquí:
En primer lugar, hay poca o ninguna penalización en el rendimiento al tener bloques try-catch en tu código. Esto no debería ser una consideración al tratar de evitar tenerlos en su aplicación. El golpe de rendimiento solo entra en juego cuando se lanza una excepción.
Cuando se lanza una excepción además de las operaciones de desenrollado de la pila, etc., que otros han mencionado, debe tener en cuenta que un montón de cosas relacionadas con el tiempo de ejecución / reflexión ocurren para completar los miembros de la clase de excepción, como el seguimiento de la pila objeto y los diversos tipos de miembros, etc.
Creo que esta es una de las razones por las cuales el consejo general de si vas a volver a lanzar la excepción es
throw;
en lugar de arrojar la excepción nuevamente o construir una nueva ya que en esos casos toda esa información de la pila se vuelve a registrar mientras que en el lanzamiento simple se conserva todo.