¿Los bloques try / catch perjudican el rendimiento cuando no se lanzan excepciones?

274

Durante una revisión de código con un empleado de Microsoft, encontramos una gran sección de código dentro de un try{} bloque. Ella y un representante de TI sugirieron que esto puede tener efectos en el rendimiento del código. De hecho, sugirieron que la mayor parte del código debe estar fuera de los bloques try / catch, y que solo se deben verificar las secciones importantes. El empleado de Microsoft agregó y dijo que un próximo libro blanco advierte contra bloques de prueba / captura incorrectos.

Miré a mi alrededor y descubrí que puede afectar las optimizaciones , pero parece que solo se aplica cuando una variable se comparte entre ámbitos.

No estoy preguntando sobre la capacidad de mantenimiento del código, o incluso manejando las excepciones correctas (el código en cuestión necesita ser refactorizado, sin duda). Tampoco me refiero al uso de excepciones para el control de flujo, esto es claramente incorrecto en la mayoría de los casos. Esos son asuntos importantes (algunos son más importantes), pero no es el foco aquí.

¿Cómo afectan los bloques try / catch al rendimiento cuando no se lanzan excepciones ?

Kobi
fuente
147
"El que sacrificaría la corrección por el rendimiento tampoco merece".
Joel Coehoorn
16
Dicho esto, no siempre se debe sacrificar la corrección por el rendimiento.
Dan Davies Brackett
19
¿Qué tal simple curiosidad?
Samantha Branham el
63
@ Joel: Quizás Kobi solo quiere saber la respuesta por curiosidad. Saber si el rendimiento será mejor o peor no necesariamente significa que va a hacer algo loco con su código. ¿No es buena la búsqueda del conocimiento por sí misma?
LukeH
66
Aquí hay un buen algoritmo para saber si hacer este cambio o no. Primero, establezca objetivos de rendimiento significativos basados ​​en el cliente. En segundo lugar, escriba el código para que sea correcto y claro primero. Tercero, pruébalo contra tus objetivos. Cuarto, si cumples tus objetivos, deja el trabajo temprano y ve a la playa. Quinto, si no cumple con sus objetivos, use un generador de perfiles para encontrar el código que es demasiado lento. Sexto, si ese código resulta demasiado lento debido a un controlador de excepciones innecesario, solo entonces elimine el controlador de excepciones. Si no, arregle el código que en realidad es demasiado lento. Luego vuelve al paso tres.
Eric Lippert

Respuestas:

203

Revisalo.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Salida:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

En milisegundos:

449
416

Nuevo código:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Nuevos resultados:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334
Ben M
fuente
24
¿Puedes probarlos en orden inverso también para asegurarte de que la compilación JIT no haya tenido un efecto en el primero?
JoshJordan
28
Programas como este difícilmente parecen buenos candidatos para probar el impacto del manejo de excepciones, demasiado de lo que sucedería en los bloques normales de prueba {} catch {} se optimizará. Puede que salga a almorzar en eso ...
LorenVS
30
Esta es una compilación de depuración. El JIT no los optimiza.
Ben M
77
Esto no es cierto en absoluto, piensa en ello. ¿Cuántas veces usas try try in a loop? La mayoría de las veces usarás loop en un try.c
Athiwat Chunlakhan
9
De Verdad? "¿Cómo afectan los bloques try / catch al rendimiento cuando no se lanzan excepciones?"
Ben M
105

Después de ver todas las estadísticas con try / catch y sin try / catch, la curiosidad me obligó a mirar hacia atrás. hacia para ver qué se genera para ambos casos. Aquí está el código:

C#:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C#:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

No soy un experto en IL, pero podemos ver que se crea un objeto de excepción local en la cuarta línea .locals init ([0] class [mscorlib]System.Exception ex)después de que las cosas son bastante iguales que para el método sin intentar / atrapar hasta la línea diecisiete IL_0021: leave.s IL_002f. Si ocurre una excepción, el control salta a la línea; de lo IL_0025: ldloc.0contrario, saltamos a la etiquetaIL_002d: leave.s IL_002f y la función regresa.

Puedo suponer con seguridad que si no se producen excepciones, es la sobrecarga de crear variables locales para contener solo objetos de excepción y una instrucción de salto.

TheIdiot de la aldea
fuente
33
Bueno, el IL incluye un bloque try / catch en la misma notación que en C #, por lo que esto realmente no muestra cuánto sobrecarga significa un try / catch detrás de escena. Solo que el IL no agrega mucho más, no significa lo mismo, ya que no se agrega algo en el código ensamblado compilado. El IL es solo una representación común de todos los lenguajes .NET. ¡NO es un código de máquina!
asombro
64

No. Si las optimizaciones triviales que impide un bloque try / finally realmente tienen un impacto medible en su programa, probablemente no debería usar .NET en primer lugar.

John Kugelman
fuente
10
Ese es un punto excelente: en comparación con los otros elementos de nuestra lista, este debería ser minúsculo. Debemos confiar en que las características básicas del lenguaje se comporten correctamente y optimizar lo que podemos controlar (sql, índices, algoritmos).
Kobi
3
Piensa en los lazos apretados, amigo. Por ejemplo, el bucle donde lee y deserializa objetos de un flujo de datos de socket en el servidor del juego y trata de exprimir todo lo que puede. Entonces, MessagePack para la serialización de objetos en lugar de binaryformatter, y usa ArrayPool <byte> en lugar de simplemente crear conjuntos de bytes, etc. En estos escenarios, ¿cuál es el impacto de múltiples (quizás anidados) intente atrapar bloques dentro del ciclo cerrado. El compilador se saltará algunas optimizaciones y también la variable de excepción va a Gen0 GC. Todo lo que digo es que hay "Algunos" escenarios en los que todo tiene un impacto.
tcwicks
35

Explicación bastante completa del modelo de excepción .NET.

Tidbits de rendimiento de Rico Mariani: Costo de excepción: Cuándo lanzar y cuándo no

El primer tipo de costo es el costo estático de tener un manejo de excepciones en su código. Las excepciones administradas en realidad funcionan comparativamente bien aquí, con lo que quiero decir que el costo estático puede ser mucho más bajo que decir en C ++. ¿Por qué es esto? Bueno, el costo estático realmente se incurre en dos tipos de lugares: Primero, los sitios reales de try / finally / catch / throw donde hay código para esas construcciones. En segundo lugar, en el código no administrado, existe el costo de sigilo asociado con el seguimiento de todos los objetos que deben destruirse en caso de que se produzca una excepción. Hay una cantidad considerable de lógica de limpieza que debe estar presente y la parte furtiva es que incluso el código que no

Dmitriy Zaslavskiy:

Según la nota de Chris Brumme: también hay un costo relacionado con el hecho de que JIT no está realizando alguna optimización en presencia de capturas

arul
fuente
1
Lo que pasa con C ++ es que una gran parte de la biblioteca estándar arrojará excepciones. No hay nada opcional sobre ellos. Debe diseñar sus objetos con algún tipo de política de excepción, y una vez que lo haya hecho, no habrá más costos de sigilo.
David Thornley
Las afirmaciones de Rico Mariani son completamente incorrectas para C ++ nativo. "El costo estático puede ser mucho más bajo que decir en C ++" - Esto simplemente no es cierto. Sin embargo, no estoy seguro de cuál fue el diseño del mecanismo de excepción en 2003 cuando se escribió el artículo. C ++ realmente no tiene ningún costo cuando no se lanzan excepciones , sin importar cuántos bloques try / catch tenga y dónde se encuentren.
BJovke
1
@BJovke C ++ "manejo de excepciones de costo cero" solo significa que no hay costo de tiempo de ejecución cuando no se lanzan excepciones, pero todavía hay un costo de tamaño de código importante debido a todos los códigos de limpieza que llaman a los destructores en las excepciones. Además, si bien no se genera ningún código específico de excepción en la ruta de código normal, el costo aún no es cero, porque la posibilidad de excepciones todavía restringe el optimizador (por ejemplo, las cosas necesarias en caso de una excepción deben permanecer en algún lugar -> los valores pueden descartarse de manera menos agresiva -> asignación de registro menos eficiente)
Daniel
24

La estructura es diferente en el ejemplo de Ben M . Se extenderá por encima dentro del interiorfor bucle que hará que no sea una buena comparación entre los dos casos.

Lo siguiente es más preciso para la comparación donde el código completo para verificar (incluida la declaración de variables) está dentro del bloque Try / Catch:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Cuando ejecuté el código de prueba original de Ben M , noté una diferencia tanto en la configuración de Debug como en la de Releas.

Esta versión, noté una diferencia en la versión de depuración (en realidad más que la otra versión), pero no hubo diferencia en la versión de lanzamiento.

Conclusión :
según estas pruebas, creo que podemos decir que Try / Catch tener un pequeño impacto en el rendimiento.

EDITAR:
intenté aumentar el valor del bucle de 10000000 a 1000000000, y volví a ejecutarlo en Release para obtener algunas diferencias en el lanzamiento, y el resultado fue este:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

Usted ve que el resultado es inconsecuente. ¡En algunos casos, la versión que usa Try / Catch es realmente más rápida!

temor
fuente
1
También he notado esto, a veces es más rápido con try / catch. Lo he comentado sobre la respuesta de Ben. Sin embargo, a diferencia de 24 votantes, no me gusta este tipo de evaluación comparativa, no creo que sea una buena indicación. El código es más rápido en este caso, pero ¿lo será siempre?
Kobi
55
¿No prueba esto que su máquina estaba haciendo una variedad de otras tareas al mismo tiempo? El tiempo transcurrido nunca es una buena medida, debe usar un generador de perfiles que registre el tiempo del procesador, no el tiempo transcurrido.
Colin Desmond
2
@Kobi: Estoy de acuerdo en que esta no es la mejor manera de comparar si va a publicarlo como una prueba de que su programa se ejecuta más rápido que otro o algo así, pero puede darle a usted como desarrollador una indicación de que un método funciona mejor que otro . En este caso, creo que podemos decir que las diferencias (al menos para la configuración de Release) son ignorables.
asombro
1
No estás cronometrando try/catchaquí. Estás cronometrando 12 intentos / capturas en la sección crítica contra bucles de 10M. El ruido del bucle erradicará cualquier influencia que tenga el try / catch. si, en cambio, coloca el try / catch dentro del ciclo cerrado y compara con / sin, terminaría con el costo del try / catch. (sin duda, tal codificación no es una buena práctica en general, pero si desea cronometrar la sobrecarga de una construcción, así es como lo hace). Hoy en día, BenchmarkDotNet es la herramienta de referencia para tiempos de ejecución confiables.
Abel
15

Probé el impacto real de un try..catch en un circuito cerrado, y es demasiado pequeño por sí solo para ser un problema de rendimiento en cualquier situación normal.

Si el ciclo funciona muy poco (en mi prueba hice un x++), puede medir el impacto del manejo de excepciones. El ciclo con manejo de excepciones tardó aproximadamente diez veces más en ejecutarse.

Si el ciclo hace un trabajo real (en mi prueba llamé al método Int32.Parse), el manejo de excepciones tiene muy poco impacto para ser medible. Obtuve una diferencia mucho mayor al cambiar el orden de los bucles ...

Guffa
fuente
11

intente que los bloques de captura tengan un impacto insignificante en el rendimiento, pero la excepción Lanzar puede ser bastante considerable, probablemente aquí es donde su compañero de trabajo estaba confundido.

RHicke
fuente
8

El try / catch TIENE impacto en el rendimiento.

Pero no es un gran impacto. la complejidad try / catch es generalmente O (1), al igual que una asignación simple, excepto cuando se colocan en un bucle. Entonces tienes que usarlos sabiamente.

Aquí hay una referencia sobre el rendimiento try / catch (aunque no explica la complejidad del mismo, pero está implícito). Echa un vistazo a la sección Lanzar menos excepciones

Isaac
fuente
3
La complejidad es O (1) no significa demasiado. Por ejemplo, si equipa una sección de código que se llama con mucha frecuencia con try-catch (o menciona un bucle), los O (1) s podrían sumar un número medible al final.
Csaba Toth
6

En teoría, un bloque try / catch no tendrá efecto en el comportamiento del código a menos que realmente ocurra una excepción. Sin embargo, hay algunas circunstancias excepcionales en las que la existencia de un bloque try / catch puede tener un efecto importante, y algunas infrecuentes pero apenas oscuras en las que el efecto puede ser notable. La razón de esto es ese código dado como:

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

el compilador puede optimizar la declaración1 basándose en el hecho de que la declaración2 está garantizada para ejecutarse antes que la declaración3. Si el compilador puede reconocer que thing1 no tiene efectos secundarios y que thing2 realmente no usa x, puede omitir de manera segura thing1 por completo. Si [como en este caso] thing1 fuera costoso, eso podría ser una optimización importante, aunque los casos en que thing1 es costoso también son los que el compilador sería menos probable que optimice. Supongamos que el código fue cambiado:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Ahora existe una secuencia de eventos donde la instrucción3 podría ejecutarse sin que la instrucción2 se haya ejecutado. Incluso si nada en el código para thing2podría arrojar una excepción, sería posible que otro subproceso pudiera usar un Interlocked.CompareExchangepara notar que qse borró y establecerlo Thread.ResetAbort, y luego realizar una Thread.Abort()instrucción anterior2 en la que escribió su valor x. Luego, catchse ejecutaría Thread.ResetAbort()[a través del delegado q], permitiendo que la ejecución continúe con la instrucción3. Tal secuencia de eventos sería, por supuesto, excepcionalmente improbable, pero se requiere un compilador para generar código que funcione de acuerdo con las especificaciones, incluso cuando ocurran tales eventos improbables.

En general, es mucho más probable que el compilador note oportunidades de omitir bits de código simples que los complejos, y por lo tanto, sería raro que un try / catch pueda afectar mucho el rendimiento si nunca se lanzan excepciones. Aún así, hay algunas situaciones en las que la existencia de un bloque try / catch puede evitar optimizaciones que, de no ser por el try / catch, hubieran permitido que el código se ejecute más rápido.

Super gato
fuente
5

Aunque " Prevenir es mejor que manejar ", en la perspectiva del rendimiento y la eficiencia, podríamos elegir el try-catch en lugar de la prevaricación. Considere el siguiente código:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Aquí está el resultado:

With Checking: 20367
With Exception: 13998
Ted Oddman
fuente
4

Consulte la discusión sobre la implementación de try / catch para obtener una explicación de cómo funcionan los bloques try / catch y cómo algunas implementaciones tienen una sobrecarga elevada y otras tienen sobrecarga cero, cuando no se producen excepciones. En particular, creo que la implementación de Windows de 32 bits tiene una alta sobrecarga, y la implementación de 64 bits no.

Ira Baxter
fuente
Lo que describí son dos enfoques diferentes para implementar excepciones. Los enfoques se aplican igualmente a C ++ y C #, así como al código administrado / no administrado. ¿Qué eligió MS para su C #? No sé exactamente, pero la arquitectura de manejo de excepciones de las aplicaciones a nivel de máquina proporcionadas por MS utiliza el esquema más rápido. Me sorprendería un poco si la implementación de C # para 64 bits no lo usara.
Ira Baxter