¿Qué tan lentas son las excepciones .NET?


143

No quiero una discusión sobre cuándo y no lanzar excepciones. Deseo resolver un problema simple. El 99% de las veces, el argumento para no arrojar excepciones gira en torno a que son lentos, mientras que el otro lado afirma (con prueba de referencia) que la velocidad no es el problema. He leído numerosos blogs, artículos y publicaciones relacionadas con un lado u otro. Entonces, ¿cuál es?

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


13
hay mentiras, malditas mentiras y puntos de referencia. :)
gbjbaanb

Desafortunadamente, varias respuestas altamente votadas aquí omitieron que la pregunta es "¿qué tan lentas son las excepciones?", Y específicamente se les pidió que evitaran el tema de con qué frecuencia usarlas. Una respuesta simple a la pregunta que realmente se hace es ..... En Windows CLR, las excepciones son 750 veces más lentas que los valores de retorno.
David Jeske

Respuestas:


207

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 artículos cortos sobre esto. Hay críticas sobre el aspecto de referencia, que en su mayoría se reducen a "en la vida real habría más pila por recorrer, por lo que volaría el caché, etc.", pero usar códigos de error para avanzar en la pila también volar el caché, así que no lo veo como un argumento particularmente bueno.

Solo para dejarlo claro: no apoyo el uso de excepciones donde no son lógicas. Por ejemplo, int.TryParsees completamente apropiado para convertir datos de un usuario. Es apropiado cuando se lee un archivo generado por máquina, donde la falla 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". "

Al usar excepciones en "solo circunstancias razonables", nunca he visto una aplicación cuyo rendimiento se haya visto significativamente afectado por las excepciones. Básicamente, las excepciones no deberían ocurrir a menudo a menos que tenga problemas de corrección significativos, y si tiene problemas de corrección significativos, entonces el rendimiento no es el mayor problema que enfrenta.


2
desafortunadamente, a las personas se les dice que las excepciones son gratuitas, úselas para una funcionalidad trivial 'correcta', deben usarse como usted dice, cuando las cosas han salido mal, en circunstancias 'excepcionales'
gbjbaanb

44
Sí, las personas deben ser conscientes de que hay un costo de rendimiento asociado con el uso de excepciones de manera inapropiada. Sólo creo que es un no-problema cuando se utilizan adecuadamente :)
Jon Skeet

77
@PaulLockwood: Diría que si tienes más de 200 excepciones por segundo , estás abusando de las excepciones. Claramente no es un evento "excepcional" si ocurre 200 veces por segundo. Tenga en cuenta la última oración de la respuesta: "Básicamente, las excepciones no deberían ocurrir a menudo a menos que tenga problemas de corrección significativos, y si tiene problemas de corrección significativos, entonces el rendimiento no es el mayor problema que enfrenta".
Jon Skeet

44
@PaulLockwood: Mi punto es que si tienes más de 200 excepciones por segundo, eso probablemente ya indique que estás abusando de las excepciones. No me sorprende que ese sea el caso, pero significa que el aspecto del rendimiento no sería mi primera preocupación, sino el abuso de las excepciones. Una vez que haya eliminado todos los usos inapropiados de las excepciones, no esperaría que fueran una parte importante del rendimiento.
Jon Skeet

44
@DavidJeske: Has perdido el punto de la respuesta. Obviamente, lanzar una excepción es mucho más lento que devolver un valor normal. Nadie está discutiendo eso. La pregunta es si son demasiado lentos. Si usted está en una adecuada situación para lanzar una excepción y que está causando un problema de rendimiento, entonces usted probablemente tiene problemas más grandes - porque sugiere que hay una enorme cantidad equivocada con su sistema. Normalmente, el problema es realmente que estás usando excepciones cuando son inapropiadas para empezar.
Jon Skeet

31

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) (advertencia2: 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 de CPU del anillo 0. Obviamente, en el mundo real, estará haciendo muchos otros trabajos, por lo que la extraña excepción no se notará en absoluto, pero si los usa para el flujo del programa, espere que su aplicación se vea afectada. Este es otro ejemplo de la máquina de marketing de MS que nos perjudica. Recuerdo que un microsoftie nos contó cómo incurrieron en una sobrecarga absolutamente cero, 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, hay un serio problema de rendimiento a largo plazo con excepciones y esto debe tenerse en cuenta en su decisión.


Puedo mencionar esto en las pruebas del mundo real, donde un tipo anulable provoca una excepción que se genera muchas veces en un "este es el flujo normal del programa", que terminó con problemas de rendimiento significativos. Siempre recuerde, las excepciones son para casos excepcionales, ¡no le crea a nadie que diga lo contrario o terminará con un hilo de github como ese!
gbjbaanb

8

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

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

El flujo de trabajo de excepción en una aplicación .NET usa excepciones de primera y segunda oportunidad. Para todas las excepciones, incluso si las está capturando y manejando, el objeto de excepción aún se crea y el marco aún debe recorrer la pila para buscar un controlador. Si atrapas y vuelves a lanzar, por supuesto, eso llevará más tiempo: obtendrás una excepción de primera oportunidad, cógela, vuelve a lanzarla, causando otra excepción de primera oportunidad, que luego no encuentra un controlador, lo que causa Una excepción de segunda oportunidad.

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

Además, de acuerdo con mi copia de "Pruebas de rendimiento de aplicaciones web de Microsoft .NET" escrita por el equipo de ACE:

"El manejo de excepciones es costoso. La ejecución del subproceso involucrado se suspende mientras CLR se repite a través de la pila de llamadas en busca del controlador de excepciones correcto, y cuando se encuentra, el controlador de excepciones y algunos bloques finalmente deben tener la oportunidad de ejecutarse antes de que se pueda realizar el procesamiento regular ".

Mi propia experiencia en el campo demostró que reducir las excepciones ayudó significativamente al rendimiento. Por supuesto, hay otras cosas que debe tener en cuenta al realizar pruebas de rendimiento, por ejemplo, si se dispara su E / S de disco, o si sus consultas están en segundos, entonces ese debería ser su enfoque. Pero encontrar y eliminar excepciones debería ser una parte vital de esa estrategia.


1
Nada de lo que haya escrito contradice la afirmación de que las excepciones son lentas si se lanzan. Solo ha hablado de situaciones en las que se arrojan. Cuando ha "ayudado significativamente al rendimiento" al eliminar las excepciones: 1) ¿Fueron verdaderas condiciones de error, o simplemente un error del usuario ?
Jon Skeet

2) ¿Estaba ejecutando el depurador o no?
Jon Skeet

Lo único que puede hacer con una excepción si no lo lanza es crearlo como un objeto, lo que no tiene sentido. Estar bajo el depurador o no, no importa, seguirá siendo más lento. Sí, hay ganchos que sucederán con un depurador conectado, pero aún es lento
Cory Foy

44
Lo sé, formé parte del equipo Premier de MSFT. :) Digamos, lotes, miles por segundo en algunos casos extremos que vimos. Nada como conectarse con un depurador en vivo y solo ver excepciones tan rápido como puedas leer. Los Ex son lentos, por lo que se conecta a un DB, por lo que lo hace cuando tiene sentido.
Cory Foy

55
Cory, creo que el punto de "solo lento cuando se lanzan" es que no tienes que preocuparte por el rendimiento debido a la mera presencia de bloqueos de captura / finalmente. Es decir, estos en sí mismos no causan un impacto en el rendimiento, solo la aparición de una instancia de excepción real.
Ian Horwill

6

El argumento, según tengo entendido, no es que arrojar excepciones es malo, son lentas per se. En cambio, se trata de usar la construcción throw / catch como una forma de primera clase de controlar la lógica de aplicación normal, en lugar de las construcciones condicionales más tradicionales.

A menudo, en la lógica de aplicación normal, realiza bucles donde la misma acción se repite miles / millones de veces. En este caso, con un perfil muy simple (vea la clase Cronómetro), puede ver por sí mismo que lanzar una excepción en lugar de decir una declaración simple if puede resultar ser mucho más lenta.

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

Resulta que en muchos casos esto se debió a que los clientes intentaban la conversión de tipos de valores en un bucle y cada intento fallaba. Se lanzó una excepción de conversión y luego fue capturada por un controlador de excepciones 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 tales posibles problemas de rendimiento.

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


¿Pensé que esas funciones de "prueba" también usan excepciones?
greg

1
Estas funciones de "Prueba" no lanzan excepciones internamente por una falla al analizar el valor de entrada. Sin embargo, aún arrojan excepciones para otras situaciones de error, como ArgumentException.
Ash

Creo que esta respuesta se acerca más al centro del problema que cualquier otra. Decir 'usar excepciones solo en circunstancias razonables' realmente no responde la pregunta: la idea real es que usar excepciones de C # para el flujo de control es mucho más lento que las construcciones condicionales habituales. Podrías ser perdonado por pensar lo contrario. En OCaml, las excepciones son más o menos un GOTO y la forma aceptada de implementar un descanso cuando se usan las características imperativas. En mi caso particular, reemplazar en un ciclo cerrado int.Parse () más try / catch vs. int. TryParse () dio un aumento significativo en el rendimiento.
Hugh W

4

Mi servidor XMPP ganó un gran impulso de velocidad (lo siento, no hay números reales, meramente de observación) después de que intenté evitar que ocurrieran (como comprobar si un socket está conectado antes de intentar leer más datos) y darme formas de evitarlos. (los métodos mencionados de TryX). Eso fue con solo unos 50 usuarios virtuales activos (chateando).


3
Los números serían útiles, desafortunadamente :( Las cosas como las operaciones de socket deberían superar ampliamente los costos de excepción, ciertamente cuando no se depuran. Si alguna vez lo compara completamente, me interesaría mucho ver los resultados.
Jon Skeet

3

Solo para agregar mi propia experiencia reciente a esta discusión: en línea con la mayoría de lo que está escrito anteriormente, encontré que lanzar excepciones es extremadamente lento cuando se realiza de forma repetida, incluso sin el depurador en ejecución. Acabo de aumentar el rendimiento de un gran programa que estoy escribiendo en un 60% al cambiar alrededor de cinco líneas de código: cambiar a un modelo de código de retorno en lugar de lanzar excepciones. De acuerdo, el código en cuestión se ejecutaba miles de veces y potencialmente arrojaba miles de excepciones antes de que lo cambiara. Por lo tanto, estoy de acuerdo con la declaración anterior: arroje excepciones cuando algo importante realmente salga mal, no como una forma de controlar el flujo de la aplicación en cualquier situación "esperada".


2

Si los compara con los códigos de retorno, son lentos como el infierno. Sin embargo, como afirmaron los pósters anteriores, no desea lanzar el funcionamiento normal del programa, por lo que solo obtiene el impacto cuando se produce un problema y en la gran mayoría de los casos el rendimiento ya no importa (ya que la excepción implica un bloqueo de la carretera de todos modos).

Definitivamente vale la pena usarlos sobre los códigos de error, las ventajas son una gran OMI.


2

Nunca he tenido ningún problema de rendimiento con excepciones. Uso 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 espagueti.

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 pila atrapa y vuelve a lanzar), sí, serán lentas, porque tienes sobrecarga cada captura / lanzamiento.

Pero si lanza en la parte inferior de la pila y atrapa en la parte superior (sustituye una cadena completa de códigos de retorno con un solo lanzamiento / captura), todas las operaciones costosas se realizan una vez.

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

Solo para probar 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 muestran al principio, entre los códigos de retorno y las excepciones, al final. Se necesita el mismo tiempo en ambos casos. Tenga en cuenta que debe compilar con optimizaciones.


2

Pero mono lanza una excepción 10 veces más rápido que el modo independiente .net, y el modo independiente .net lanza una excepción 60 veces más rápido que el modo de 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");

1

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.


1

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

Este alto costo de 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 sean capturadas y lanzadas correctamente en diferentes tiempos de ejecución e idiomas. Sin embargo, es muy muy lento.

Las excepciones en el tiempo de ejecución Mono (en cualquier plataforma) son mucho más rápidas, ya que no se integra con SEH. Sin embargo, hay una pérdida de funcionalidad al pasar excepciones a través de múltiples tiempos de ejecución porque no usa 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));         }
        }
    }
}


}

55
Además de perder el punto de la pregunta, no use DateTime. Ahora para los puntos de referencia: use el cronómetro, que está diseñado para medir el tiempo transcurrido. Aquí no debería ser un problema, ya que está midiendo períodos de tiempo razonablemente largos, pero vale la pena adquirir el hábito.
Jon Skeet

Por el contrario, la pregunta es "son excepciones lentas", punto. Pidió específicamente evitar el tema de cuándo lanzar excepciones, porque ese tema oscurece los hechos. ¿Cuáles son los resultados de las excepciones?
David Jeske

0

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

Cuando la ruta de ejecución entra en un bloque de "prueba", no ocurre nada mágico. No hay instrucciones de 'prueba', y ningún 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 usan en tiempo de ejecución cada vez que se genera una excepción. El motor de ejecución camina por la pila buscando la primera llamada contenida en un bloque de prueba. Cualquier sobrecarga asociada con el manejo de excepciones ocurre solo cuando se lanzan excepciones.


1
Sin embargo, la presencia de excepciones puede afectar la optimización: los métodos con manejadores de excepciones explícitos son más difíciles de alinear y el reordenamiento de instrucciones está limitado por ellos.
Eamon Nerbonne

-1

Cuando se escriben clases / funciones para que otros las usen, parece difícil decir cuándo son apropiadas las excepciones. Hay algunas partes útiles de BCL que tuve que abandonar e ir a pinvoke porque arrojan excepciones en lugar de devolver errores. En algunos casos, puede solucionarlo, pero para otros, como System.Management and Performance Counters, hay usos en los que necesita hacer 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 usar en un bucle y existe la posibilidad de una gran cantidad de iteraciones, use el patrón Try .. o alguna otra forma de exponer los errores además de las excepciones. E incluso entonces, 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 producen cuando las cosas son tan excepcionales que es necesario mirar el rastro de la pila y ver qué salió mal y luego solucionarlo. Así que prácticamente reescribí partes de BCL para usar el manejo de errores basado en el patrón Try .. en lugar de excepciones.


2
Esto no parece encajar en la declaración del cartel " No quiero una discusión sobre cuándo y no lanzar excepciones ".
hrbrmstr
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.