¿Intentar / atrapar bloques perjudica el rendimiento cuando no se lanzan las excepciones?

Durante una revisión del código con un empleado de Microsoft, encontramos una gran sección de código dentro de un bloque try{} . Ella y un representante de TI sugirieron que esto puede tener efectos sobre el rendimiento del código. De hecho, sugirieron que la mayor parte del código debería estar fuera de los bloques try / catch, y que solo las secciones importantes deberían ser revisadas. El empleado de Microsoft agregó y dijo que una próxima nota técnica advierte sobre bloques de try / catch incorrectos.

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

No estoy preguntando acerca de la mantenibilidad del código, o incluso manejando las excepciones correctas (el código en cuestión necesita volver a factorizar, 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 el enfoque aquí.

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

EDITAR: Estoy agregando un bounty. Hay respuestas interesantes, pero me gustaría obtener más información.

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 

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

DO#:

 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 

DO#:

 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 muy .locals init ([0] class [mscorlib]System.Exception ex) al método sin try / atrapa hasta la línea diecisiete IL_0021: leave.s IL_002f . Si se produce una excepción, el control salta a la línea IL_0025: ldloc.0 contrario, IL_0025: ldloc.0 a la etiqueta IL_002d: leave.s IL_002f y la función retorna.

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

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

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

Costos de rendimiento de Rico Mariani: costo de la excepción: cuándo lanzar y cuándo no

El primer tipo de costo es el costo estático de tener control de excepción en su código. Las excepciones administradas realmente son comparativamente buenas aquí, lo que significa que el costo estático puede ser mucho menor que decir en C ++. ¿Por qué es esto? Bien, 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 esos constructos. En segundo lugar, en el código no controlado, existe el costo oculto asociado con el seguimiento de todos los objetos que se deben destruir 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 astuta es que incluso el código que no tira o atrapa, o que de otra forma tiene un uso manifiesto de las excepciones, sigue teniendo la carga de saber cómo limpiarlo.

Dmitriy Zaslavskiy:

Según la nota de Chris Brumme: también hay un costo relacionado con el hecho de que JIT no está realizando algunas optimizaciones en presencia de captura

La estructura es diferente en el ejemplo de Ben M. Se extenderá sobre su cabeza dentro del ciclo for interno lo que hará que no sea una buena comparación entre los dos casos.

Lo siguiente es más preciso para la comparación donde todo el código a verificar (incluida la statement de variables) se encuentra 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 depuración como en la de liberación.

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

Concluir :
Con base en estas pruebas, creo que podemos decir que Try / Catch tiene un pequeño impacto en el rendimiento.

EDITAR:
Intenté boost el valor del ciclo de 10000000 a 1000000000, y volví a ejecutarlo en Release para obtener algunas diferencias en la versión, y el resultado fue el siguiente:

  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 intrascendente. ¡En algunos casos, la versión que usa Try / Catch es realmente más rápida!

try..catch impacto real de una try..catch en un circuito cerrado, y es demasiado pequeño para ser un problema de rendimiento en cualquier situación normal.

Si el ciclo hace muy poco trabajo (en mi prueba hice una x++ ), puede medir el impacto del manejo de excepciones. El ciclo con manejo de excepciones tomó aproximadamente diez veces más tiempo para ejecutarse.

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

prueba los bloques de captura tienen un impacto insignificante en el rendimiento, pero la excepción El lanzamiento puede ser bastante considerable, es probable que este sea el lugar donde tu compañero de trabajo estaba confundido.

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 debes usarlos sabiamente.

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

En teoría, un bloque try / catch no tendrá ningún efecto en el comportamiento del código a menos que ocurra una excepción. Sin embargo, existen algunas circunstancias excepcionales en las que la existencia de un bloque de prueba / captura puede tener un efecto importante, y algunas poco comunes, pero poco claras, donde el efecto puede ser notable. La razón para esto es que el 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 comstackdor puede optimizar la instrucción1 en función del hecho de que la statement2 está garantizada para ejecutarse antes que la instrucción3. Si el comstackdor puede reconocer que thing1 no tiene efectos secundarios y thing2 en realidad no usa x, puede omitir thing1 por completo. Si [como en este caso] thing1 era costoso, podría ser una gran optimización, aunque los casos donde thing1 es caro también son los que el comstackdor tendría menos probabilidades de optimizar. 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 statement3 podría ejecutarse sin que se haya ejecutado statement2. Incluso si nada en el código para thing2 pudiera arrojar una excepción, sería posible que otro subproceso podría usar un Interlocked.CompareExchange para notar que q se borró y establecerlo en Thread.ResetAbort , y luego realizar un Thread.Abort() antes statement2 escribió su valor en x . Luego, la catch ejecutaría Thread.ResetAbort() [a través del delegado q ], permitiendo que la ejecución continúe con statement3. Tal secuencia de eventos sería, por supuesto, excepcionalmente improbable, pero se requiere un comstackdor para generar código que funcione de acuerdo con la especificación incluso cuando ocurran tales eventos improbables.

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

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