¿Qué parte de arrojar una excepción es costosa?

En Java, usar throw / catch como parte de la lógica cuando no hay un error es generalmente una mala idea (en parte) porque lanzar y atrapar una excepción es costoso, y hacerlo muchas veces en un bucle suele ser mucho más lento que otros estructuras de control que no implican lanzar excepciones.

Mi pregunta es, ¿se incurre en el costo del lanzamiento / captura en sí mismo, o al crear el objeto Excepción (ya que obtiene mucha información de tiempo de ejecución, incluida la stack de ejecución)?

En otras palabras, si lo hago

Exception e = new Exception(); 

pero no lo arrojes, ¿es eso lo que cuesta tirar, o es el manejo del tiro + captura lo que es costoso?

No estoy preguntando si poner código en un bloque try / catch se sum al costo de ejecutar ese código, estoy preguntando si capturar la excepción es la parte costosa, o crear (llamando al constructor) la excepción es la parte costosa .

Otra forma de preguntar esto es: si hago una instancia de Exception y la lanzo y la atrapo una y otra vez, ¿sería mucho más rápido que crear una nueva excepción cada vez que lanzo?

Crear un objeto de excepción no es más caro que crear otros objetos regulares. El costo principal está oculto en el método nativo fillInStackTrace que recorre la stack de llamadas y recostack toda la información necesaria para generar un seguimiento de stack: clases, nombres de métodos, números de línea, etc.

El mito sobre los altos costos de excepción proviene del hecho de que la mayoría de los constructores de fillInStackTrace implícitamente llaman fillInStackTrace . Sin embargo, hay un constructor para crear un Throwable sin un rastro de stack. Le permite hacer throwables que son muy rápidos para crear instancias. Otra forma de crear excepciones ligeras es anular fillInStackTrace .


Ahora, ¿qué hay de tirar una excepción?
De hecho, depende de dónde se atrape una excepción lanzada.

Si está atrapado en el mismo método (o, más precisamente, en el mismo contexto, dado que el contexto puede incluir varios métodos debido a la alineación), throw es tan rápido y simple como goto (por supuesto, después de la comstackción de JIT).

Sin embargo, si un bloque catch está en algún lugar más profundo en la stack, entonces JVM necesita desenrollar los fotogtwigs de la stack, y esto puede llevar mucho más tiempo. Toma más tiempo, si hay bloques o métodos synchronized involucrados, porque desenrollar implica la liberación de monitores que son propiedad de los marcos de stack eliminados.


Podría confirmar las afirmaciones anteriores con los puntos de referencia adecuados, pero afortunadamente no necesito hacerlo, ya que todos los aspectos ya están perfectamente cubiertos en el puesto de Alexey Shipilëv, ingeniero de rendimiento de HotSpot: El excepcional rendimiento de la excepción Lil ‘ .

La primera operación en la mayoría de los constructores Throwable es completar el seguimiento de la stack, que es donde se encuentra la mayor parte del gasto.

Sin embargo, hay un constructor protegido con un indicador para deshabilitar el seguimiento de la stack. Este constructor es accesible cuando se extiende Exception también. Si crea un tipo de excepción personalizado, puede evitar la creación de rastreo de stack y obtener un mejor rendimiento a expensas de una menor información.

Si crea una única excepción de cualquier tipo por medios normales, puede volver a lanzarla muchas veces sin la sobrecarga de completar el seguimiento de la stack. Sin embargo, su traza de stack reflejará dónde se construyó, no dónde fue arrojada en una instancia particular.

Las versiones actuales de Java intentan optimizar la creación de trazas de stack. Se invoca el código nativo para completar el seguimiento de la stack, que registra la traza en una estructura nativa de menor peso. Los objetos Java StackTraceElement correspondientes se crean de forma perezosa a partir de este registro solo cuando se getStackTrace() , printStackTrace() u otros métodos que requieren el rastreo.

Si elimina la generación de seguimiento de stack, el otro costo principal es desenrollar la stack entre el tiro y la captura. Cuantos menos cuadros intermedios se encuentren antes de capturar la excepción, más rápido será esto.

Diseñe su progtwig de forma que solo se emitan excepciones en casos realmente excepcionales, y las optimizaciones como estas son difíciles de justificar.

Aquí hay una buena reseña sobre Excepciones.

http://shipilev.net/blog/2014/exceptional-performance/

La conclusión es que la construcción de trazas de stack y el desenrollado de la stack son las piezas caras. El siguiente código aprovecha una función en 1.7 donde podemos activar y desactivar rastreos de stack. Podemos usar esto para ver qué tipo de costos tienen diferentes escenarios

Los siguientes son tiempos para la creación de Objetos solo. He agregado String aquí para que pueda ver que sin la stack que se está escribiendo casi no hay diferencia en la creación de un objeto JavaException y una String . Con la escritura de stack activa, la diferencia es dramática, es decir, al menos un orden de magnitud más lenta.

 Time to create million String objects: 41.41 (ms) Time to create million JavaException objects with stack: 608.89 (ms) Time to create million JavaException objects without stack: 43.50 (ms) 

A continuación, se muestra cuánto tiempo tardó en regresar de un lanzamiento a una profundidad particular un millón de veces.

 |Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)| | 16| 1428| 243| 588 (%)| | 15| 1763| 393| 449 (%)| | 14| 1746| 390| 448 (%)| | 13| 1703| 384| 443 (%)| | 12| 1697| 391| 434 (%)| | 11| 1707| 410| 416 (%)| | 10| 1226| 197| 622 (%)| | 9| 1242| 206| 603 (%)| | 8| 1251| 207| 604 (%)| | 7| 1213| 208| 583 (%)| | 6| 1164| 206| 565 (%)| | 5| 1134| 205| 553 (%)| | 4| 1106| 203| 545 (%)| | 3| 1043| 192| 543 (%)| 

Lo siguiente es casi seguro una simplificación excesiva

Si tomamos una profundidad de 16 con la stack de escritura activada, la creación de objetos tarda aproximadamente el ~ 40% del tiempo, el seguimiento de stack real representa la gran mayoría de esto. ~ 93% de crear instancias del objeto JavaException se debe a que se tomó el seguimiento de la stack. Esto significa que desenrollar la stack en este caso toma el otro 50% del tiempo.

Cuando desactivamos las cuentas de creación de objeto de traza de stack para una fracción mucho más pequeña, es decir, 20%, y el desenrollado de stack ahora representa el 80% del tiempo.

En ambos casos, el desenrollado de la stack requiere una gran parte del tiempo total.

 public class JavaException extends Exception { JavaException(String reason, int mode) { super(reason, null, false, false); } JavaException(String reason) { super(reason); } public static void main(String[] args) { int iterations = 1000000; long create_time_with = 0; long create_time_without = 0; long create_string = 0; for (int i = 0; i < iterations; i++) { long start = System.nanoTime(); JavaException jex = new JavaException("testing"); long stop = System.nanoTime(); create_time_with += stop - start; start = System.nanoTime(); JavaException jex2 = new JavaException("testing", 1); stop = System.nanoTime(); create_time_without += stop - start; start = System.nanoTime(); String str = new String("testing"); stop = System.nanoTime(); create_string += stop - start; } double interval_with = ((double)create_time_with)/1000000; double interval_without = ((double)create_time_without)/1000000; double interval_string = ((double)create_string)/1000000; System.out.printf("Time to create %d String objects: %.2f (ms)\n", iterations, interval_string); System.out.printf("Time to create %d JavaException objects with stack: %.2f (ms)\n", iterations, interval_with); System.out.printf("Time to create %d JavaException objects without stack: %.2f (ms)\n", iterations, interval_without); JavaException jex = new JavaException("testing"); int depth = 14; int i = depth; double[] with_stack = new double[20]; double[] without_stack = new double[20]; for(; i > 0 ; --i) { without_stack[i] = jex.timerLoop(i, iterations, 0)/1000000; with_stack[i] = jex.timerLoop(i, iterations, 1)/1000000; } i = depth; System.out.printf("|Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%%)|\n"); for(; i > 0 ; --i) { double ratio = (with_stack[i] / (double) without_stack[i]) * 100; System.out.printf("|%5d| %14.0f| %15.0f| %2.0f (%%)| \n", i + 2, with_stack[i] , without_stack[i], ratio); //System.out.printf("%d\t%.2f (ms)\n", i, ratio); } } private int thrower(int i, int mode) throws JavaException { ExArg.time_start[i] = System.nanoTime(); if(mode == 0) { throw new JavaException("without stack", 1); } throw new JavaException("with stack"); } private int catcher1(int i, int mode) throws JavaException{ return this.stack_of_calls(i, mode); } private long timerLoop(int depth, int iterations, int mode) { for (int i = 0; i < iterations; i++) { try { this.catcher1(depth, mode); } catch (JavaException e) { ExArg.time_accum[depth] += (System.nanoTime() - ExArg.time_start[depth]); } } //long stop = System.nanoTime(); return ExArg.time_accum[depth]; } private int bad_method14(int i, int mode) throws JavaException { if(i > 0) { this.thrower(i, mode); } return i; } private int bad_method13(int i, int mode) throws JavaException { if(i == 13) { this.thrower(i, mode); } return bad_method14(i,mode); } private int bad_method12(int i, int mode) throws JavaException{ if(i == 12) { this.thrower(i, mode); } return bad_method13(i,mode); } private int bad_method11(int i, int mode) throws JavaException{ if(i == 11) { this.thrower(i, mode); } return bad_method12(i,mode); } private int bad_method10(int i, int mode) throws JavaException{ if(i == 10) { this.thrower(i, mode); } return bad_method11(i,mode); } private int bad_method9(int i, int mode) throws JavaException{ if(i == 9) { this.thrower(i, mode); } return bad_method10(i,mode); } private int bad_method8(int i, int mode) throws JavaException{ if(i == 8) { this.thrower(i, mode); } return bad_method9(i,mode); } private int bad_method7(int i, int mode) throws JavaException{ if(i == 7) { this.thrower(i, mode); } return bad_method8(i,mode); } private int bad_method6(int i, int mode) throws JavaException{ if(i == 6) { this.thrower(i, mode); } return bad_method7(i,mode); } private int bad_method5(int i, int mode) throws JavaException{ if(i == 5) { this.thrower(i, mode); } return bad_method6(i,mode); } private int bad_method4(int i, int mode) throws JavaException{ if(i == 4) { this.thrower(i, mode); } return bad_method5(i,mode); } protected int bad_method3(int i, int mode) throws JavaException{ if(i == 3) { this.thrower(i, mode); } return bad_method4(i,mode); } private int bad_method2(int i, int mode) throws JavaException{ if(i == 2) { this.thrower(i, mode); } return bad_method3(i,mode); } private int bad_method1(int i, int mode) throws JavaException{ if(i == 1) { this.thrower(i, mode); } return bad_method2(i,mode); } private int stack_of_calls(int i, int mode) throws JavaException{ if(i == 0) { this.thrower(i, mode); } return bad_method1(i,mode); } } class ExArg { public static long[] time_start; public static long[] time_accum; static { time_start = new long[20]; time_accum = new long[20]; }; } 

Los marcos de stack en este ejemplo son pequeños en comparación con lo que normalmente encontrarías.

Puedes echar un vistazo al bytecode usando javap

 javap -c -v -constants JavaException.class 

es decir, esto es para el método 4 …

  protected int bad_method3(int, int) throws JavaException; flags: ACC_PROTECTED Code: stack=3, locals=3, args_size=3 0: iload_1 1: iconst_3 2: if_icmpne 12 5: aload_0 6: iload_1 7: iload_2 8: invokespecial #6 // Method thrower:(II)I 11: pop 12: aload_0 13: iload_1 14: iload_2 15: invokespecial #17 // Method bad_method4:(II)I 18: ireturn LineNumberTable: line 63: 0 line 64: 12 StackMapTable: number_of_entries = 1 frame_type = 12 /* same */ Exceptions: throws JavaException 

La creación de la Exception con un seguimiento de stack null requiere aproximadamente tanto tiempo como el bloque de throw y el try-catch juntos. Sin embargo, llenar el seguimiento de la stack lleva un promedio de 5 veces más .

Creé el siguiente punto de referencia para demostrar el impacto en el rendimiento. -Djava.compiler=NONE el -Djava.compiler=NONE a la configuración de ejecución para deshabilitar la optimización del comstackdor. Para medir el impacto de construir el seguimiento de la stack, extendí la clase Exception para aprovechar el constructor sin stack:

 class NoStackException extends Exception{ public NoStackException() { super("",null,false,false); } } 

El código de referencia es el siguiente:

 public class ExceptionBenchmark { private static final int NUM_TRIES = 100000; public static void main(String[] args) { long throwCatchTime = 0, newExceptionTime = 0, newObjectTime = 0, noStackExceptionTime = 0; for (int i = 0; i < 30; i++) { throwCatchTime += throwCatchLoop(); newExceptionTime += newExceptionLoop(); newObjectTime += newObjectLoop(); noStackExceptionTime += newNoStackExceptionLoop(); } System.out.println("throwCatchTime = " + throwCatchTime / 30); System.out.println("newExceptionTime = " + newExceptionTime / 30); System.out.println("newStringTime = " + newObjectTime / 30); System.out.println("noStackExceptionTime = " + noStackExceptionTime / 30); } private static long throwCatchLoop() { Exception ex = new Exception(); //Instantiated here long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { try { throw ex; //repeatedly thrown } catch (Exception e) { // do nothing } } long stop = System.currentTimeMillis(); return stop - start; } private static long newExceptionLoop() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { Exception e = new Exception(); } long stop = System.currentTimeMillis(); return stop - start; } private static long newObjectLoop() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { Object o = new Object(); } long stop = System.currentTimeMillis(); return stop - start; } private static long newNoStackExceptionLoop() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { NoStackException e = new NoStackException(); } long stop = System.currentTimeMillis(); return stop - start; } } 

Salida:

 throwCatchTime = 19 newExceptionTime = 77 newObjectTime = 3 noStackExceptionTime = 15 

Esto implica que la creación de una NoStackException es aproximadamente tan cara como arrojar repetidamente la misma Exception . También muestra que la creación de una Exception y el llenado de su rastro de stack lleva aproximadamente 4 veces más.

Esta parte de la pregunta …

Otra forma de preguntar esto es: si hago una instancia de Exception y la lanzo y la atrapo una y otra vez, ¿sería mucho más rápido que crear una nueva excepción cada vez que lanzo?

Parece estar preguntándose si la creación de una excepción y el almacenamiento en caché en algún lugar mejora el rendimiento. Sí lo hace. Es lo mismo que desactivar la stack que se está escribiendo en la creación del objeto porque ya está hecho.

Estos son los tiempos que tengo, por favor lea la advertencia después de esto …

 |Depth| WriteStack(ms)| !WriteStack(ms)| Diff(%)| | 16| 193| 251| 77 (%)| | 15| 390| 406| 96 (%)| | 14| 394| 401| 98 (%)| | 13| 381| 385| 99 (%)| | 12| 387| 370| 105 (%)| | 11| 368| 376| 98 (%)| | 10| 188| 192| 98 (%)| | 9| 193| 195| 99 (%)| | 8| 200| 188| 106 (%)| | 7| 187| 184| 102 (%)| | 6| 196| 200| 98 (%)| | 5| 197| 193| 102 (%)| | 4| 198| 190| 104 (%)| | 3| 193| 183| 105 (%)| 

Por supuesto, el problema con esto es que el seguimiento de la stack ahora apunta al lugar donde instanciaste el objeto, no desde donde fue lanzado.

Usando la respuesta de @ AustinD como punto de partida, realicé algunos ajustes. Código en la parte inferior.

Además de agregar el caso en el que una instancia de Exception se lanza repetidamente, también desactivé la optimización del comstackdor para que podamos obtener resultados de rendimiento precisos. -Djava.compiler=NONE a los argumentos de VM, según esta respuesta . (En eclipse, edite la Configuración de ejecución → Argumentos para establecer este argumento de máquina virtual)

Los resultados:

 new Exception + throw/catch = 643.5 new Exception only = 510.7 throw/catch only = 115.2 new String (benchmark) = 669.8 

Por lo tanto, crear la excepción cuesta aproximadamente 5 veces más que tirar + atraparla. Suponiendo que el comstackdor no optimiza mucho del costo.

A modo de comparación, esta es la misma ejecución de prueba sin deshabilitar la optimización:

 new Exception + throw/catch = 382.6 new Exception only = 379.5 throw/catch only = 0.3 new String (benchmark) = 15.6 

Código:

 public class ExceptionPerformanceTest { private static final int NUM_TRIES = 1000000; public static void main(String[] args) { double numIterations = 10; long exceptionPlusCatchTime = 0, excepTime = 0, strTime = 0, throwTime = 0; for (int i = 0; i < numIterations; i++) { exceptionPlusCatchTime += exceptionPlusCatchBlock(); excepTime += createException(); throwTime += catchBlock(); strTime += createString(); } System.out.println("new Exception + throw/catch = " + exceptionPlusCatchTime / numIterations); System.out.println("new Exception only = " + excepTime / numIterations); System.out.println("throw/catch only = " + throwTime / numIterations); System.out.println("new String (benchmark) = " + strTime / numIterations); } private static long exceptionPlusCatchBlock() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { try { throw new Exception(); } catch (Exception e) { // do nothing } } long stop = System.currentTimeMillis(); return stop - start; } private static long createException() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { Exception e = new Exception(); } long stop = System.currentTimeMillis(); return stop - start; } private static long createString() { long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { Object o = new String("" + i); } long stop = System.currentTimeMillis(); return stop - start; } private static long catchBlock() { Exception ex = new Exception(); //Instantiated here long start = System.currentTimeMillis(); for (int i = 0; i < NUM_TRIES; i++) { try { throw ex; //repeatedly thrown } catch (Exception e) { // do nothing } } long stop = System.currentTimeMillis(); return stop - start; } }