¿Qué parte de lanzar una excepción es cara?


En Java, usar throw/catch como parte de la lógica cuando no hay realmente 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 otras estructuras de control que no implican lanzar excepciones.

Mi pregunta es, es el costo incurrido en el tiro/captura en sí, o al crear el objeto de Excepción (ya que obtiene una gran cantidad de información de tiempo de ejecución, incluida la ejecución stack)?

En otras palabras, si lo hago

Exception e = new Exception();

Pero no tirarlo, es que la mayor parte del costo de lanzar, o es el tiro + catch manejo lo que es costoso?

No estoy preguntando si poner código en un bloque try/catch aumenta el costo de ejecutar ese código, estoy preguntando si capturar la Excepción es la parte costosa, o crear (llamar al constructor para) la Excepción es la parte costosa.

Otra forma de preguntar esto es, si hice una instancia de Excepción y lo lanzó y lo atrapó una y otra vez, ¿sería significativamente más rápido que crear una nueva Excepción cada vez que lanzo?

Author: Martin Carney, 2016-03-31

6 answers

Crear un objeto de excepción no es más caro que crear otros objetos regulares. El costo principal está oculto en nativo fillInStackTrace método que recorre la pila de llamadas y recopila toda la información necesaria para construir un seguimiento de pila: 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 Throwable implícitamente llaman fillInStackTrace. Sin embargo, hay un constructor para crear un Throwable sin un rastro de pila. Le permite hacer lanzables que son muy rápidos de instanciar. Otra forma de crear excepciones ligeras es anular fillInStackTrace.


Ahora, ¿qué hay de lanzar una excepción?
De hecho, depende de donde una excepción lanzada es atrapada.

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

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


Podría confirmar las afirmaciones anteriores con puntos de referencia adecuados, pero afortunadamente no necesito hacerlo, ya que todos los aspectos ya están perfectamente cubierto en el post de HotSpot performance engineer Alexey Shipilëv: El Rendimiento Excepcional de Lil' Excepción.

 248
Author: apangin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-03 03:09:17

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

Hay, sin embargo, un constructor protegido con una bandera para deshabilitar el seguimiento de la pila. Este constructor también es accesible cuando se extiende Exception. Si crea un tipo de excepción personalizado, puede evitar la creación de trazas de pila y obtener un mejor rendimiento a expensas de menos información.

Si crea una única excepción de cualquier tipo por medios normales, puede volver a lanzarlo muchas veces sin la sobrecarga de llenar el rastro de la pila. Sin embargo, su traza de pila reflejará dónde se construyó, no dónde se lanzó en una instancia en particular.

Las versiones actuales de Java hacen algunos intentos para optimizar la creación de trazas de pila. Se invoca código nativo para rellenar el seguimiento de pila, que registra el seguimiento en una estructura nativa más ligera. Java CorrespondienteStackTraceElement los objetos se crean perezosamente a partir de este registro solo cuando el getStackTrace(), printStackTrace(), u otros métodos que requieren el seguimiento se llaman.

Si elimina la generación de trazas de pila, el otro costo principal es desenrollar la pila entre el lanzamiento y la captura. Cuantos menos fotogramas intervenidos se encuentren antes de que se capte la excepción, más rápido será esto.

Diseñe su programa para que las excepciones se produzcan solo en casos verdaderamente excepcionales, y optimizaciones como estas son difíciles de justificar.

 71
Author: erickson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-01 15:14:19

Aquí hay un buen artículo sobre las excepciones.

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

La conclusión es que la construcción de trazas de pila y el desbobinado de la pila son las piezas caras. El siguiente código aprovecha una característica en 1.7 donde podemos activar y desactivar los trazos de pila. Luego podemos usar esto para ver qué tipo de costos tienen diferentes escenarios

Los siguientes son tiempos para la creación de objetos solamente. He añadido String aquí para que puede ver que sin la pila escrita casi no hay diferencia en crear un Objeto JavaException y un String. Con la escritura de pila activada, la diferencia es dramática, es decir, al menos un orden de magnitud más lento.

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)

Lo siguiente muestra cuánto tiempo se 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 con toda seguridad una simplificación excesiva...

Si tomamos una profundidad de 16 con stack writing on, entonces la creación de objetos es tomando aproximadamente ~40% del tiempo, el seguimiento de pila real representa la gran mayoría de esto. ~93% de instanciar el objeto JavaException se debe a que se está tomando el seguimiento de la pila. Esto significa que desenrollar la pila en este caso está tomando el otro 50% del tiempo.

Cuando desactivamos la creación de objetos de seguimiento de pila cuenta con un fraction ie 20% y stack unwinding ahora representan el 80% del tiempo.

En ambos casos, el desenrollado de la pila toma 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 pila 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
 25
Author: Harry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-07 23:37:54

La creación del Exception con un trazo de pila null toma aproximadamente tanto tiempo como el bloque throw y try-catch juntos. Sin embargo, completar el seguimiento de la pila toma en promedio 5 veces más.

He creado el siguiente punto de referencia para demostrar el impacto en el rendimiento. Agregué -Djava.compiler=NONE a la configuración de ejecución para deshabilitar la optimización del compilador. Para medir el impacto de la construcción de la traza de pila, extendí la clase Exception para aprovechar la pila libre constructor:

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 crear un NoStackException es aproximadamente tan caro como lanzar repetidamente el mismo Exception. También muestra que crear un Exception y completar su seguimiento de pila toma aproximadamente 4x más tiempo.

 12
Author: Austin D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-31 23:15:01

Esta parte de la pregunta...

Otra forma de preguntar esto es, si hice una instancia de Excepción y lo tiró y lo atrapó una y otra vez, sería significativamente más rápido que crear una nueva Excepción cada vez que lanzo?

Parece estar preguntando si crear una excepción y almacenarla en caché en algún lugar mejora el rendimiento. Sí lo hace. Es lo mismo que desactivar la pila está escrito en la creación de objetos, porque ya se ha 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 su seguimiento de pila ahora apunta a donde instanció el objeto, no desde donde fue lanzado.

 4
Author: Harry,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-02 00:55:21

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

Además de agregar el caso en el que una instancia de excepción se lanza repetidamente, también desactivé la optimización del compilador para que podamos obtener resultados de rendimiento precisos. Agregué -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 VM)

Los resultados:

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

Así que crear la excepción cuesta alrededor 5x tanto como lanzar + atraparlo. Suponiendo que el compilador no optimiza gran parte del costo.

Para la comparación, aquí está 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;
    }
}
 3
Author: Martin Carney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:43