¿Cuál es la sobrecarga real de try / catch en C #?

94

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 proviene esta sobrecarga y cuál es su impacto real?

JC Grubbs
fuente

Respuestas:

52

No soy un experto en implementaciones de lenguaje (así que tómate esto con un grano de sal), pero creo que uno de los mayores costos es desenrollar la pila y almacenarla para el seguimiento de la pila. Sospecho que esto sucede solo cuando se lanza la excepción (pero no lo sé), y si es así, este sería un costo oculto de tamaño decente cada vez que se lanza una excepción ... así que no es como si estuvieras saltando de un solo lugar en el código a otro, están sucediendo muchas cosas.

No creo que sea un problema siempre y cuando esté utilizando excepciones para un comportamiento EXCEPCIONAL (por lo que no es su ruta típica y esperada a través del programa).

Mike Stone
fuente
33
Más precisamente: intentar es barato, atrapar es barato, lanzar es caro. Si evita tratar de atrapar, el lanzamiento sigue siendo caro.
Programador de Windows
4
Mmmm, el marcado no funciona en los comentarios. Para volver a intentarlo, las excepciones son por errores, no por "comportamiento excepcional" o condiciones: blogs.msdn.com/kcwalina/archive/2008/07/17/…
HTTP 410
2
@Windows programador Estadísticas / fuente, por favor?
Kapé
100

Tres puntos a destacar aquí:

  • En primer lugar, hay poca o NINGUNA penalización de rendimiento en tener bloques try-catch en su código. Esto no debe tenerse en cuenta al intentar evitar tenerlos en su aplicación. El impacto 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 tienen lugar, lo que otros han mencionado, debe tener en cuenta que ocurre una gran cantidad de cosas relacionadas con el tiempo de ejecución / reflexión para completar los miembros de la clase de excepción, como el seguimiento de la pila. objeto y los diversos miembros de tipo, etc.

  • Creo que esta es una de las razones por las que el consejo general si va a volver a lanzar la excepción es simplemente en throw;lugar de lanzar la excepción nuevamente o construir una nueva, ya que en esos casos toda la información de la pila se recopila mientras que en el simple tirarlo todo se conserva.

Shaun Austin
fuente
41
Además: cuando vuelve a generar una excepción como "lanzar ex", pierde el seguimiento de la pila original y lo reemplaza por el seguimiento de la pila ACTUAL; raramente lo que se busca. Si simplemente "lanza", se conserva el seguimiento de la pila original en la excepción.
Eddie
@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki
21

¿Está preguntando sobre la sobrecarga de usar try / catch / finalmente cuando no se lanzan excepciones, o la sobrecarga de usar excepciones para controlar el flujo del proceso? Esto último es algo parecido a usar una barra 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 pérdidas de caché adicionales debido a que la excepción lanzada accede a datos residentes que normalmente no se encuentran en la caché.
  • Puede esperar errores de página adicionales debido a la excepción lanzada al acceder al código no residente y a los 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 de captura y final en base a la IP actual y la IP de retorno de cada trama hasta que se maneje la excepción más el bloque de filtro.
    • costo de construcción adicional y resolución de nombres para crear los marcos con fines de diagnóstico, incluida la lectura de metadatos, etc.
    • Ambos elementos anteriores suelen acceder a códigos y datos "fríos", por lo que es probable que se produzcan errores de página si tiene presión de memoria:

      • el CLR intenta colocar el código y los datos que se usan con poca frecuencia lejos de los datos que se usan con frecuencia para mejorar la localidad, por lo que esto funciona en su contra porque está forzando el frío a ser caliente.
      • el costo de las fallas de la página dura, si las hubiera, empequeñecerá todo lo demás.
  • Las situaciones de captura típicas suelen ser profundas, por lo que los efectos anteriores tenderían 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 están dañando significativamente su desempeño, tiene problemas en términos de su uso de las excepciones más allá del desempeño.

HTTP 410
fuente
6

En mi experiencia, la mayor sobrecarga consiste en lanzar una excepción y manejarla. Una vez trabajé en un proyecto donde se usó 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 de la capa de presentación y, a menudo, se solicitó para 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 prueba se llenó más con datos de prueba, esto condujo a una desaceleración muy visible al abrir nuevos formularios, etc.

Así que lo refactoricé a lo siguiente, que, de acuerdo con mediciones posteriores de quick 'n dirty, 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.

Tobi
fuente
1
¿Por qué querrías lanzar una excepción aquí? Podrías manejar el caso de no tener los derechos en el acto.
ThunderGr
@ThunderGr eso es realmente lo que cambié, haciéndolo dos órdenes de magnitud más rápido.
Tobi
5

Contrariamente a las teorías comúnmente aceptadas, try/ catchpuede tener implicaciones de rendimiento significativas, ¡y eso es si se lanza una excepción o no!

  1. Deshabilita algunas optimizaciones automáticas (por diseño) y, en algunos casos, inyecta código de depuración , como puede esperar de una ayuda de depuración . Siempre habrá gente que no esté de acuerdo conmigo en este punto, pero el lenguaje lo requiere y el desmontaje lo demuestra, por lo que, por definición del diccionario, esas personas están delirando .
  2. Puede afectar negativamente al mantenimiento. Este es en realidad el problema más importante aquí, pero dado que se eliminó mi última respuesta (que se centró casi por completo en él), intentaré 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 Microsoft MVP a lo largo de los años, y confío que pueda encontrarlos fácilmente, pero StackOverflow se preocupa tanto por el contenido, por lo que proporcionaré enlaces a algunos de ellos como evidencia de relleno :

También existe esta respuesta que muestra la diferencia entre el código desensamblado con y sin usar try/ catch.

Parece tan obvio que no es una sobrecarga que es manifiestamente observables en la generación de código, y que los gastos generales, incluso parece ser reconocido por la gente que valora Microsoft! Sin embargo, estoy repitiendo Internet ...

Sí, hay docenas de instrucciones MSIL adicionales para una línea de código trivial, y eso ni siquiera cubre las optimizaciones deshabilitadas, por lo que técnicamente es una microoptimización.


Publiqué una respuesta hace años que se eliminó porque se enfocaba en la productividad de los programadores (la macro-optimización).

Esto es lamentable, ya que es probable que no se ahorren unos pocos nanosegundos aquí y allá del tiempo de la CPU para compensar muchas horas acumuladas de optimización manual por parte de los humanos. ¿Por qué paga más tu jefe: una hora de tu tiempo o una hora con la computadora encendida? ¿En qué momento desconectamos 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, me basé en las diferencias entre dos fragmentos de código.

Usando try/ catch:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Sin usar 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 (mencionado anteriormente), lo que probablemente ni siquiera sería necesario si no fuera por el problema try/ catch, luego al desplazarse por código fuente ... ¡Uno de ellos tiene cuatro líneas adicionales de basura estándar!

A medida que se introducen más y más campos en una clase, toda esta basura repetitiva se acumula (tanto en el código fuente como en el código desensamblado) mucho más allá de los niveles razonables. Cuatro líneas extra por campo, y siempre son las mismas líneas ... ¿No nos enseñaron a evitar repetirnos? Supongo que podríamos ocultar el try/ catchdetrás de alguna abstracción casera, pero ... entonces podríamos evitar las excepciones (es decir, el uso Int.TryParse).

Este ni siquiera es un ejemplo complejo; He visto intentos de instanciar nuevas clases en try/ catch. Tenga en cuenta que todo el código dentro del constructor podría ser descalificado 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 , en lugar de que el compilador está haciendo exactamente lo que se le dice que haga ?

Suponiendo que dicho constructor lanza una excepción y, como resultado, se activa algún error, el desarrollador de mantenimiento deficiente tiene que rastrearlo. Puede que no sea una tarea tan fácil, ya que, a diferencia del código espagueti de la pesadilla goto , try/ catchpuede causar desorden en tres dimensiones , ya que podría subir en la pila no solo a otras partes del mismo método, sino también a otras clases y métodos. , todo lo cual será observado por el desarrollador de mantenimiento, ¡de la manera difícil ! Sin embargo, se nos dice que "goto es peligroso", ¡eh!

Al final menciono, try/ catchtiene su beneficio, que es que está diseñado para deshabilitar optimizaciones . ¡Es, por así decirlo, una ayuda para la depuración ! Para eso fue diseñado y es para lo que debería usarse como ...

Supongo que también es un punto positivo. Se puede usar para deshabilitar optimizaciones que de otro modo podrían paralizar los algoritmos de paso de mensajes seguros y cuerdos para aplicaciones multiproceso, y para detectar posibles condiciones de carrera;) Ese es el único escenario en el que puedo pensar para usar try / catch. Incluso eso tiene alternativas.


Qué hacer optimizaciones try, catchy finallydesactivar?

AKA

¿Cómo están try, catchy finallyútil como la depuración de las ayudas?

son barreras de escritura. Esto viene del estándar:

12.3.3.13 declaraciones Try-catch

Para una declaración stmt de la forma:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • El estado de asignación definida de v al comienzo del bloque try 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 definido 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 del bloque try y cada bloque catch-i (para cada i de 1 an ).

En otras palabras, al comienzo de cada trydeclaración:

  • Todas las asignaciones realizadas a objetos visibles antes de ingresar la trydeclaración deben estar completas, lo que requiere un bloqueo de hilo para comenzar, ¡lo que lo hace útil para depurar condiciones de carrera!
  • el compilador no tiene permitido:
    • eliminar las asignaciones de variables no utilizadas que definitivamente se han asignado antes de la trydeclaración
    • reorganizar o fusionar cualquiera de sus asignaciones internas (es decir, vea mi primer enlace, si aún no lo ha hecho).
    • izar asignaciones sobre esta barrera, para retrasar la asignación a una variable que sabe que no se usará hasta más tarde (si es que se usará) o para adelantar de manera preventiva asignaciones posteriores para hacer posibles otras optimizaciones ...

Una historia similar es válida para cada catchdeclaración; suponga que dentro de su trydeclaración (o un constructor o función que invoca, etc.) asigna a esa variable que de otro modo no tendría 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 asignación debe haberse completado antes de catchingresar al bloque.

Por lo que vale, finallycuenta una historia igualmente degradante :

12.3.3.14 Declaraciones de prueba finalmente

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 comienzo del bloque try es el mismo que el estado de asignación definida de v al comienzo de stmt .
• El estado de asignación definida de v al comienzo del bloque final 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 del bloque try o vse asigna definitivamente en el punto final del bloque final Si se realiza una transferencia de flujo de control (como una instrucción goto ) que comienza dentro del bloque try y termina fuera del bloque try , entonces v también se considera definitivamente asignado en ese controlar la transferencia de flujo si v se asigna definitivamente en el punto final del bloque final . (Esto no es un solo si, si v se asigna definitivamente 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-finalmente

Análisis de asignación definida para una declaración de prueba , captura y finalmente de la forma:

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 - finalmente adjuntando una declaración try - catch :

try { 
    try   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block
autista
fuente
3

Sin mencionar que si está dentro de un método al que se llama con frecuencia, 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 se puede detectar fácilmente de otra manera.

Entonces, para concluir todo lo escrito aquí:
1) Use bloques try..catch para detectar errores inesperados, casi sin penalización de rendimiento.
2) No use excepciones para errores exceptuados si puede evitarlo.

dotmad
fuente
3

Escribí un artículo sobre esto hace un tiempo 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 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 fuera del bucle si es posible.

El problema de rendimiento clave con los bloques try / catch es cuando realmente detecta una excepción. Esto puede agregar un retraso notable a su solicitud. 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 utilizar el manejo de excepciones para operaciones normales. Como su nombre indica, son excepcionales y debes hacer todo lo posible para evitar que se lancen. No debe utilizarlos como parte del flujo esperado de un programa que funciona correctamente.

BlackWasp
fuente
2

Hice una entrada de blog sobre este tema el año pasado. Echale un vistazo. La conclusión es que casi no hay costo por un bloque de prueba si no ocurre ninguna excepción, y en mi computadora portátil, una excepción fue de aproximadamente 36 μs. Eso podría ser menos de lo que esperaba, pero tenga en cuenta que esos resultados están en una pila poco profunda. Además, las primeras excepciones son realmente lentas.

Hafthor
fuente
No pude llegar a su blog (la conexión se está agotando; ¿está usando try/ catchdemasiado? Je, je), pero parece que está discutiendo con la especificación del idioma y algunos MVP de MS que también han escrito blogs sobre el tema, proporcionando medidas al contrario de tu consejo ... Estoy abierto a la sugerencia de que la investigación que he hecho es incorrecta, pero tendré que leer la entrada de tu blog para ver qué dice.
autista
Además de la publicación de blog de @ Hafthor, aquí hay otra publicación de blog con código escrito específicamente para probar las diferencias de rendimiento de velocidad. Según los resultados, si se produce una excepción incluso solo el 5% del tiempo, el código de manejo de excepciones se ejecuta 100 veces más lento en general que el código de manejo sin excepciones. El artículo se dirige específicamente a los métodos try-catchbloque vs tryparse(), pero el concepto es el mismo.
2

Es mucho más fácil escribir, depurar y mantener código libre de mensajes de error del compilador, mensajes de advertencia de análisis de código y excepciones aceptadas de rutina (en particular, excepciones que se lanzan en un lugar y se aceptan en otro). Debido a que es más fácil, el código estará, en promedio, mejor escrito y con menos errores.

Para mí, ese programador y la sobrecarga de calidad es el principal argumento en contra del uso de try-catch para el flujo del proceso.

La sobrecarga informática de las excepciones es insignificante en comparación y, por lo general, pequeña en términos de la capacidad de la aplicación para cumplir con los requisitos de rendimiento del mundo real.


fuente
@Ritchard T, ¿por qué? En comparación con el programador y la sobrecarga de calidad , es insignificante.
ThunderGr
1

Realmente me gusta la publicación del blog de Hafthor y, para agregar mis dos centavos a esta discusión, me gustaría decir que siempre ha sido fácil para mí hacer que DATA LAYER arroje solo un tipo de excepción (DataAccessException). De esta manera, mi BUSINESS LAYER sabe qué excepción esperar y la detecta. 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 volver a lanzar.

Yo diría que no dude en usar try..catch siempre que sea necesario y utilícelo sabiamente.

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;
}

fuente
2
David, ¿envolverías la llamada a 'DeleteGallery' en un bloque try / catch?
Rob
Dado que DeleteGallery es una función booleana, me parece que lanzar una excepción allí no es útil. Esto requeriría que la llamada a DeleteGallery se incluyera en un bloque try / catch. Un if (! DeleteGallery (theid)) {// handle} me parece más significativo. en ese ejemplo específico.
ThunderGr
1

Podemos leer en Programming Languages ​​Pragmatics de Michael L. Scott que los compiladores de hoy en día no agregan ninguna sobrecarga en caso común, es decir, cuando no ocurren excepciones. Entonces, cada trabajo se realiza en tiempo de compilación. Pero cuando se lanza una excepción en tiempo de ejecución, el compilador necesita realizar una búsqueda binaria para encontrar la excepción correcta y esto sucederá para 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 en su lugar, probablemente necesitará una declaración if para cada subrutina y esto incurrirá en 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 subrutinas.

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.

Vando
fuente
El compilador está fuera de imagen en tiempo de ejecución. No tiene que ser una sobrecarga para los bloques try / catch para que el CLR puede manejar las excepciones. C # se ejecuta en .NET CLR (una máquina virtual). Me parece que la sobrecarga del bloque en sí es mínima cuando no hay una excepción, pero el costo del CLR que maneja la excepción es muy significativo.
ThunderGr
-4

Analicemos uno de los mayores costos posibles de un bloque try / catch cuando se usa donde no debería ser necesario:

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 sangría. ¿Eso es algo malo?

Para colmo de males, un estudiante decide hacer un bucle mientras 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 cuando se usa try / catch?

try {
    for (;;)
    {
        x = int.Parse(...);
        ...
    }
}
catch
{
    ...
}

Los bloques Try / catch son formas mágicas de desperdiciar la sangría, ¡y aún no sabemos la razón por la que falló! Imagínese cómo se siente la persona que realiza la depuración, cuando el código continúa ejecutándose más allá de una falla lógica grave, en lugar de detenerse con un error de excepción obvio y agradable. Los bloques de prueba / captura son la validación / saneamiento de datos de un hombre perezoso.

Uno de los costos menores es que los bloques try / catch deshabilitan ciertas optimizaciones: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Supongo que también es un punto positivo. Se puede usar para deshabilitar optimizaciones que de otro modo podrían paralizar los algoritmos de paso de mensajes sanos y seguros para aplicaciones multiproceso, y para detectar posibles condiciones de carrera;) Ese es el único escenario en el que puedo pensar para usar try / catch. Incluso eso tiene alternativas.

Sebastián Ramadán
fuente
1
Estoy bastante seguro de que TryParse lo intenta {int x = int.Parse ("xxx"); devuelve verdadero;} captura {devuelve falso; } internamente. La sangría no es una preocupación en la pregunta, solo el rendimiento y los gastos generales.
ThunderGr
@ThunderGr Alternativamente, lea la nueva respuesta que publiqué . Contiene más enlaces, uno de los cuales es un análisis del aumento masivo del rendimiento cuando evitas Int.Parsea favor de Int.TryParse.
autista