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

256

En Java, usar tirar / atrapar como parte de la lógica cuando en realidad 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 ciclo suele ser mucho más lento que otro estructuras de control que no implican lanzar excepciones.

Mi pregunta es, ¿es el costo incurrido en el lanzamiento / captura en sí, o al crear el objeto Exception (ya que obtiene mucha información de tiempo de ejecución, incluida la pila de ejecución)?

En otras palabras, si lo hago

Exception e = new Exception();

pero no lo lance, ¿es eso la mayor parte del costo del lanzamiento, o el manejo de lanzar + atrapar es 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 atrapar la Excepción es la parte costosa o crear (llamar al constructor) la Excepción es la parte costosa .

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

Martin Carney
fuente
20
Creo que se está completando y poblando el rastro de la pila.
Elliott Frisch
12
Verifique esto: stackoverflow.com/questions/16451777/…
Jorge
"si hice una instancia de Exception y la lancé y la atrapé una y otra vez", cuando se crea la excepción, se completa su stacktrace, lo que significa que siempre será la misma stactrace, independientemente del lugar desde el que se lanzó. Si stacktrace no es importante para usted, entonces podría intentar su idea, pero esto podría hacer que la depuración sea muy difícil, si no imposible, en algunos casos.
Pshemo
2
@Pshemo No planeo hacer esto realmente en código, estoy preguntando sobre el rendimiento y usando este absurdo como un ejemplo en el que podría marcar la diferencia.
Martin Carney
@ MartinCarney He agregado una respuesta a su último párrafo, es decir, el almacenamiento en caché de una excepción tiene una ganancia de rendimiento. Si es útil, puedo agregar el código, si no, puedo eliminar la respuesta.
Harry

Respuestas:

267

Crear un objeto de excepción no es más costoso que crear otros objetos regulares. El costo principal está oculto en el fillInStackTracemétodo nativo que recorre la pila de llamadas y recopila toda la información requerida para construir un seguimiento de la 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 Throwableconstructores lo llaman implícitamente fillInStackTrace. Sin embargo, hay un constructor para crear un Throwablesin un seguimiento de pila. Le permite crear objetos arrojables que son muy rápidos para crear instancias. Otra forma de crear excepciones ligeras es anular fillInStackTrace.


¿Y qué hay de lanzar una excepción?
De hecho, depende de donde está una excepción lanzada atrapado .

Si se detecta 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 alineación), entonces throwes tan rápido y simple como goto(por supuesto, después de la compilación JIT).

Sin embargo, si un catchbloque está en algún lugar más profundo de la pila, JVM necesita desenrollar los marcos de la pila, y esto puede llevar mucho más tiempo. Se tarda aún más, si hay synchronizedbloques o métodos involucrados, porque desenrollar implica la liberación de monitores propiedad de los marcos de pila eliminados.


Podría confirmar las declaraciones anteriores con los puntos de referencia adecuados, pero afortunadamente no necesito hacer esto, ya que todos los aspectos ya están perfectamente cubiertos en la publicación del ingeniero de rendimiento de HotSpot, Alexey Shipilev: El rendimiento excepcional de Lil 'Exception .

apangin
fuente
8
Como se señaló en el artículo y se menciona aquí, el resultado es que el costo de lanzar / atrapar excepciones depende en gran medida de la profundidad de las llamadas. El punto aquí es que la afirmación "las excepciones son caras" no es realmente correcta. Una afirmación más correcta es que las excepciones "pueden" ser caras. Honestamente, creo que decir que solo se usen excepciones para "casos verdaderamente excepcionales" (como en el artículo) está demasiado redactado. Son perfectos para casi cualquier cosa fuera del flujo de retorno normal y es difícil detectar el impacto en el rendimiento de usarlos de esta manera en una aplicación real.
JimmyJames
14
Puede valer la pena cuantificar los gastos generales de las excepciones. Incluso en el peor de los casos reportados en este artículo bastante exhaustivo (lanzar y atrapar una excepción dinámica con un seguimiento de pila que realmente se consulta, 1000 cuadros de pila de profundidad), lleva 80 micro segundos. Eso puede ser significativo si su sistema necesita procesar miles de excepciones por segundo, pero de lo contrario no vale la pena preocuparse. Y ese es el peor de los casos; si sus stacktraces son un poco más sanos, o no los consulta stacktrace, podemos procesar casi un millón de excepciones por segundo.
meriton
13
Hago hincapié en esto porque muchas personas, al leer que las excepciones son "caras", nunca dejan de preguntar "caro en comparación con qué", sino que suponen que son "parte costosa de su programa", lo que rara vez lo son.
meriton
2
Hay una parte que no se menciona aquí: el costo potencial para evitar que se apliquen optimizaciones. Un ejemplo extremo sería que la JVM no se alinea para evitar rastros de pila "confusos", pero he visto (micro) puntos de referencia donde la presencia o ausencia de excepciones haría o rompería optimizaciones en C ++ anteriormente.
Matthieu M.
3
@MatthieuM. Las excepciones y los bloques try / catch no impiden que JVM se incorpore. Para los métodos compilados, las trazas reales de la pila se reconstruyen a partir de la tabla de marcos de la pila virtual almacenada como metadatos. No puedo recordar una optimización JIT que sea incompatible con try / catch. La estructura Try / catch en sí misma no agrega nada al código del método, solo existe como una tabla de excepción aparte del código.
Apangin
72

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

Sin embargo, hay 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 del seguimiento de la 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 lanzarla muchas veces sin la sobrecarga de completar el seguimiento de la pila. Sin embargo, su seguimiento de pila reflejará dónde fue construido, no dónde fue arrojado en una instancia particular.

Las versiones actuales de Java hacen algunos intentos para optimizar la creación de seguimiento de pila. El código nativo se invoca para completar el seguimiento de la pila, que registra el seguimiento en una estructura nativa más ligera. Java correspondientes StackTraceElementobjetos se crean a partir de perezosamente este registro sólo cuando los getStackTrace(), printStackTrace()son llamados, u otros métodos que requieren la traza.

Si elimina la generación de seguimiento de la pila, el otro costo principal es desenrollar la pila entre el lanzamiento y la captura. Cuantas menos tramas intermedias se encuentren antes de que se detecte la excepción, más rápido será.

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

erickson
fuente
25

Hay un buen artículo sobre Excepciones aquí.

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 partes caras. El siguiente código aprovecha una característica en la 1.7que podemos activar y desactivar los seguimientos 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 solo. He agregado Stringaquí para que pueda ver que sin la pila escrita, casi no hay diferencia en la creación de un JavaExceptionObjeto y un String. Con la escritura en 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)

A continuación se muestra el tiempo que 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 seguro una gran simplificación ...

Si tomamos una profundidad de 16 con la escritura de la pila activada, la creación de objetos lleva aproximadamente ~ 40% del tiempo, el seguimiento real de la pila representa la gran mayoría de esto. ~ El 93% de la creación de instancias del objeto JavaException se debe al seguimiento de la pila que se está tomando. Esto significa que desenrollar la pila en este caso lleva el otro 50% del tiempo.

Cuando desactivamos la creación de objetos de rastreo de pila, representa una fracción mucho menor, es decir, el 20%, y el desenrollado de pila ahora representa el 80% del tiempo.

En ambos casos, el desbobinado de la pila ocupa 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ía.

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
Harry
fuente
13

La creación de Exceptioncon un nullseguimiento de pila lleva casi tanto tiempo como el bloque throwy try-catchjuntos. Sin embargo, completar el seguimiento de la pila tarda en promedio 5 veces más .

Creé el siguiente punto de referencia para demostrar el impacto en el rendimiento. Agregué -Djava.compiler=NONEa la Configuración de ejecución para deshabilitar la optimización del compilador. Para medir el impacto de construir el seguimiento de la pila, amplié la Exceptionclase para aprovechar el constructor sin pila:

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 NoStackExceptiones aproximadamente tan costoso como tirar repetidamente lo mismo Exception. También muestra que crear Exceptiony completar su seguimiento de pila tarda aproximadamente 4 veces más.

Austin D
fuente
1
¿Podría agregar un caso más en el que cree una instancia de Excepción antes de la hora de inicio, y luego arroje + atrape repetidamente en un bucle? Eso mostraría el costo de solo lanzar + atrapar.
Martin Carney
@MartinCarney ¡Gran sugerencia! Actualicé mi respuesta para hacer precisamente eso.
Austin D
Hice algunos ajustes en su código de prueba, y parece que el compilador está haciendo una optimización que nos impide obtener números precisos.
Martin Carney
@ MartinCarney Actualicé la respuesta a la optimización del compilador de descuento
Austin D
Para su información, probablemente debería leer las respuestas a ¿Cómo escribo un micro punto de referencia correcto en Java? Sugerencia: esto no es así.
Daniel Pryden
4

Esta parte de la pregunta ...

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

Parece preguntarse si crear una excepción y almacenarla en caché en algún lugar mejora el rendimiento. Si lo hace Es lo mismo que apagar la pila que se escribe 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 rastro de pila ahora apunta a dónde instanciaron el objeto, no desde dónde fue arrojado.

Harry
fuente
3

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

Además de agregar el caso donde se lanza una instancia de Excepción repetidamente, también desactivé la optimización del compilador para que podamos obtener resultados de rendimiento precisos. Agregué -Djava.compiler=NONEa los argumentos de VM, según esta respuesta . (En eclipse, edite Ejecutar configuració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

Por lo tanto, crear la excepción cuesta aproximadamente 5 veces más que lanzarlo y atraparlo. Suponiendo que el compilador no optimiza gran parte del costo.

A modo de 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;
    }
}
Martin Carney
fuente
Desactivar la optimización = ¡gran técnica! Editaré mi respuesta original para no confundir a nadie
Austin D
3
Deshabilitar la optimización no es mejor que escribir un punto de referencia defectuoso, ya que el modo interpretado puro no tiene nada que ver con el rendimiento del mundo real. El poder de JVM es el compilador JIT, entonces, ¿cuál es el punto de medir algo que no refleja cómo funciona la aplicación real?
Apangin
2
Hay muchos más aspectos de creación, lanzamiento y captura de excepciones que los cubiertos en este 'punto de referencia'. Te sugiero que leas esta publicación .
Apangin