¿Es costoso usar bloques try-catch incluso si nunca se lanza una excepción?

189

Sabemos que es costoso detectar excepciones. Pero, ¿también es costoso usar un bloque try-catch en Java incluso si nunca se lanza una excepción?

Encontré la pregunta / respuesta Stack Overflow ¿Por qué los bloques de prueba son caros? , pero es para .NET .

jsedano
fuente
30
Realmente no tiene sentido esta pregunta. Try..catch tiene un propósito muy específico. Si lo necesitas, lo necesitas. En cualquier caso, ¿qué punto es un intento sin una trampa?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }es legal y útil
A4L
44
Ese costo tiene que ser comparado con los beneficios. No está solo. En cualquier caso, lo costoso es relativo, y hasta que sepa que no puede hacerlo, tiene sentido usar el método más obvio en lugar de no hacer algo porque podría ahorrarle un milisegundo o dos en el transcurso de una hora de ejecución del programa
Joel
44
Espero que esto no es una ventaja a una situación de tipo "códigos vamos-a-reinventar-error" ...
mikołak
66
@SAFX: con Java7 incluso puedes deshacerte del finallybloque usando untry-with-resources
a_horse_with_no_name

Respuestas:

201

trycasi no tiene gastos en absoluto. En lugar de hacer el trabajo de configurar el trytiempo de ejecución, los metadatos del código se estructuran en el momento de la compilación de modo que cuando se produce una excepción, ahora se realiza una operación relativamente costosa de subir la pila y ver si hay algunotry existen bloques que atrapen esto excepción. Desde la perspectiva de un laico, bien trypodría ser libre. En realidad, arrojar la excepción es lo que le cuesta, pero a menos que esté lanzando cientos o miles de excepciones, aún no notará el costo.


trytiene algunos costos menores asociados. Java no puede hacer algunas optimizaciones en el código en un trybloque que de otro modo haría. Por ejemplo, Java a menudo reorganizará las instrucciones en un método para que se ejecute más rápido, pero Java también debe garantizar que si se produce una excepción, la ejecución del método se observa como si sus declaraciones, tal como están escritas en el código fuente, se ejecuten en orden hasta alguna línea.

¡Porque en un trybloque se puede lanzar una excepción (en cualquier línea en el bloque de prueba! Algunas excepciones se lanzan de forma asincrónica, como llamar stopa un Thread (que está en desuso), e incluso además, OutOfMemoryError puede suceder casi en cualquier lugar) y, sin embargo, puede detectarse y el código continúa ejecutándose después con el mismo método, es más difícil razonar sobre las optimizaciones que se pueden hacer, por lo que es menos probable que sucedan. (Alguien tendría que programar el compilador para que lo haga, razonar y garantizar la corrección, etc. Sería un gran dolor para algo destinado a ser 'excepcional') Pero una vez más, en la práctica no notará cosas como esta.

Patashu
fuente
2
Algunas excepciones se lanzan de forma asincrónica , no son asíncronas sino que se lanzan en puntos seguros. y esta prueba tiene algunos costos menores asociados. Java no puede hacer algunas optimizaciones en el código en un bloque de prueba que de lo contrario sí necesitaría una referencia seria. En algún momento, es muy probable que el código esté dentro del bloque try / catch. Podría ser cierto que el bloque try / catch sería más difícil de alinear y construir una red adecuada para el resultado, pero la parte con el reordenamiento es ambigua.
Bestsss
2
¿Un try...finallybloqueo sin catchtambién impide algunas optimizaciones?
dajood
55
@Patashu "En realidad, lanzar la excepción es lo que te cuesta" Técnicamente, lanzar la excepción no es costoso; instanciar el Exceptionobjeto es lo que lleva la mayor parte del tiempo.
Austin D
72

Vamos a medirlo, ¿de acuerdo?

public abstract class Benchmark {

    final String name;

    public Benchmark(String name) {
        this.name = name;
    }

    abstract int run(int iterations) throws Throwable;

    private BigDecimal time() {
        try {
            int nextI = 1;
            int i;
            long duration;
            do {
                i = nextI;
                long start = System.nanoTime();
                run(i);
                duration = System.nanoTime() - start;
                nextI = (i << 1) | 1;
            } while (duration < 100000000 && nextI > 0);
            return new BigDecimal((duration) * 1000 / i).movePointLeft(3);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String toString() {
        return name + "\t" + time() + " ns";
    }

    public static void main(String[] args) throws Exception {
        Benchmark[] benchmarks = {
            new Benchmark("try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        try {
                            x += i;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    return x;
                }
            }, new Benchmark("no try") {
                @Override int run(int iterations) throws Throwable {
                    int x = 0;
                    for (int i = 0; i < iterations; i++) {
                        x += i;
                    }
                    return x;
                }
            }
        };
        for (Benchmark bm : benchmarks) {
            System.out.println(bm);
        }
    }
}

En mi computadora, esto imprime algo como:

try     0.598 ns
no try  0.601 ns

Al menos en este ejemplo trivial, la declaración try no tuvo un impacto medible en el rendimiento. Siéntase libre de medir los más complejos.

En términos generales, recomiendo no preocuparse por el costo de rendimiento de las construcciones de lenguaje hasta que tenga evidencia de un problema de rendimiento real en su código. O como dijo Donald Knuth : "la optimización prematura es la raíz de todo mal".

Meriton
fuente
44
mientras que tratar / no intentar es muy probable que sea el mismo en la mayoría de las JVM, el microbenchmark es terriblemente defectuoso.
Bestsss
2
bastantes niveles: ¿quiere decir que los resultados se calculan en menos de 1ns? El código compilado eliminará tanto el try / catch Y el bucle por completo (sumando el número de 1 a n es una suma trivial de progresión aritmética). Incluso si el código contiene prueba / finalmente el compilador puede probar, no hay nada que arrojar allí. El código abstracto tiene solo 2 sitios de llamada y será clonado y en línea. Hay más casos, solo busque algunos artículos sobre microbenchmark y si decide escribir un microbenchmark siempre verifique el ensamblaje generado.
Bestsss
3
Los tiempos informados son por iteración del bucle. Como una medida solo se usará si tiene un tiempo transcurrido total> 0.1 segundos (o 2 mil millones de iteraciones, que no fue el caso aquí), encuentro su afirmación de que el bucle se ha eliminado en su totalidad, es difícil de creer, porque si se eliminó el bucle, ¿qué tardó 0.1 segundos en ejecutarse?
meriton
... y de hecho, de acuerdo con -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, tanto el bucle como la adición están presentes en el código nativo generado. Y no, los métodos abstractos no están en línea, porque su interlocutor no solo se compila a tiempo (presumiblemente, porque no se invoca suficientes veces).
meriton
¿Cómo escribo un micro-benchmark correcto en Java? Stackoverflow.com/questions/504103/…
Vadzim
47

try/ catchpuede tener algún impacto en el rendimiento. Esto se debe a que evita que JVM haga algunas optimizaciones. Joshua Bloch, en "Java efectivo", dijo lo siguiente:

• Colocar el código dentro de un bloque try-catch inhibe ciertas optimizaciones que las implementaciones modernas de JVM de otro modo podrían realizar.

Evgeniy Dorofeev
fuente
25
"evita que JVM haga algunas optimizaciones" ...? ¿Podrías dar más detalles?
El Kraken
55
@ El Código Kraken dentro de los bloques de prueba (¿generalmente? ¿Siempre?) No se puede reordenar con código fuera de los bloques de prueba, como un ejemplo.
Patashu
3
Tenga en cuenta que la pregunta era si "es caro", no si "tiene algún impacto en el rendimiento".
mikołak
3
agregó un extracto de Effective Java , y esa es la biblia de java, por supuesto; a menos que haya una referencia, el extracto no dice nada. Prácticamente cualquier código en java está dentro de try / finalmente en algún nivel.
Bestsss
29

Sí, como han dicho los demás, un trybloque inhibe algunas optimizaciones en los {}personajes que lo rodean. En particular, el optimizador debe asumir que podría ocurrir una excepción en cualquier punto dentro del bloque, por lo que no hay garantía de que las declaraciones se ejecuten.

Por ejemplo:

    try {
        int x = a + b * c * d;
        other stuff;
    }
    catch (something) {
        ....
    }
    int y = a + b * c * d;
    use y somehow;

Sin el try, el valor calculado para asignar a xpodría guardarse como una "subexpresión común" y reutilizarse para asignar a y. Pero debido a que tryno hay garantía de que la primera expresión haya sido evaluada, entonces la expresión debe ser recalculada. Esto no suele ser un gran problema en el código de "línea recta", pero puede ser significativo en un bucle.

Sin embargo, debe tenerse en cuenta que esto se aplica SOLO al código JITCed. javac solo realiza una pequeña cantidad de optimización, y el intérprete de bytecode tiene un costo cero para ingresar / salir de un trybloque. (No se generan bytecodes para marcar los límites del bloque).

Y para bestsss:

public class TryFinally {
    public static void main(String[] argv) throws Throwable {
        try {
            throw new Throwable();
        }
        finally {
            System.out.println("Finally!");
        }
    }
}

Salida:

C:\JavaTools>java TryFinally
Finally!
Exception in thread "main" java.lang.Throwable
        at TryFinally.main(TryFinally.java:4)

salida de javap:

C:\JavaTools>javap -c TryFinally.class
Compiled from "TryFinally.java"
public class TryFinally {
  public TryFinally();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    Code:
       0: new           #2                  // class java/lang/Throwable
       3: dup
       4: invokespecial #3                  // Method java/lang/Throwable."<init>":()V
       7: athrow
       8: astore_1
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: ldc           #5                  // String Finally!
      14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      17: aload_1
      18: athrow
    Exception table:
       from    to  target type
           0     9     8   any
}

No "GOTO".

Hot Licks
fuente
No se generan bytecodes para marcar los límites del bloque, esto no es necesariamente: requiere que GOTO abandone el bloque, de lo contrario caerá en el catch/finallymarco.
Bestsss
@bestsss: incluso si se genera un GOTO (que no es un hecho), el costo es minúsculo, y está lejos de ser un "marcador" para un límite de bloque: el GOTO se puede generar para muchas construcciones.
Hot Licks
Nunca mencioné el costo, sin embargo, no se generan bytecodes, es una declaración falsa. Eso es todo. En realidad, no hay bloques en bytecode, los marcos no son iguales a los bloques.
Bestsss
No habrá un GOTO si el intento cae directamente en el finalmente, y hay otros escenarios en los que no habrá GOTO. El punto es que no hay nada en el orden de los códigos de byte "enter try" / "exit try".
Hot Licks
No habrá un GOTO si el intento cae directamente en el finalmente - ¡Falso! no hay ningún finallybytecode, es try/catch(Throwable any){...; throw any;}Y tiene una declaración catch con un marco y Throwable que DEBE SER definido (no nulo) y así sucesivamente. ¿Por qué intentas discutir sobre el tema, puedes verificar al menos algún bytecode? La directriz actual para impl. Finalmente, es copiar los bloques y evitar la sección goto (impl anterior) pero los bytecodes tienen que copiarse dependiendo de cuántos puntos de salida haya.
bestsss
8

Para comprender por qué no se pueden realizar las optimizaciones, es útil comprender los mecanismos subyacentes. El ejemplo más sucinto que pude encontrar se implementó en macros C en: http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <stdio.h>
#include <setjmp.h>
#define TRY do{ jmp_buf ex_buf__; switch( setjmp(ex_buf__) ){ case 0: while(1){
#define CATCH(x) break; case x:
#define FINALLY break; } default:
#define ETRY } }while(0)
#define THROW(x) longjmp(ex_buf__, x)

Los compiladores a menudo tienen dificultades para determinar si un salto se puede localizar en X, Y y Z, por lo que omiten las optimizaciones que no pueden garantizar que sean seguras, pero la implementación en sí es bastante ligera.

technosaurus
fuente
44
Estas macros C que ha encontrado para try / catch no son equivalentes a la implementación Java o C #, que emite 0 instrucciones de tiempo de ejecución.
Patashu
La implementación de Java es demasiado extensa para incluirla en su totalidad, esta es una implementación simplificada con el propósito de comprender la idea básica de cómo se pueden implementar las excepciones. Decir que emite 0 instrucciones de tiempo de ejecución es engañoso. Por ejemplo, una simple classcastexception extiende la excepción de tiempo de ejecución que extiende la excepción que se extiende arrojable que involucra: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... Eso es como decir que un caso de cambio en C es gratis si solo se usa 1 caso, todavía hay una pequeña sobrecarga de inicio.
technosaurus
1
@Patashu Todos esos bits precompilados todavía tienen que cargarse al inicio, ya sea que se usen o no. No hay forma de saber si habrá una excepción de falta de memoria durante el tiempo de ejecución en tiempo de compilación, es por eso que se llaman excepciones de tiempo de ejecución, de lo contrario serían advertencias / errores del compilador, por lo que no, no optimiza todo , todo el código para manejarlos está incluido en el código compilado y tiene un costo de inicio.
technosaurus
2
No puedo hablar sobre C. En C # y Java, el intento se implementa agregando metadatos, no código. Cuando se ingresa un bloque de prueba, no se ejecuta nada que indique esto: cuando se lanza una excepción, la pila se desenrolla y se comprueban los metadatos en busca de controladores de ese tipo de excepción (caro).
Patashu
1
Sí, en realidad implementé un intérprete de Java y un compilador de código de bytes estático y trabajé en un JITC posterior (para IBM iSeries) y puedo decirle que no hay nada que "marque" la entrada / salida del rango de prueba en los códigos de bytes, sino más bien los rangos se identifican en una tabla separada. El intérprete no hace nada especial para un rango de prueba (hasta que se genere una excepción). Un JITC (o compilador de bytecode estático) debe conocer los límites para suprimir las optimizaciones como se indicó anteriormente.
Hot Licks
8

Sin embargo, otro microbenchmark ( fuente ).

Creé una prueba en la que mido la versión del código try-catch y no-try-catch según un porcentaje de excepción. El 10% de porcentaje significa que el 10% de los casos de prueba tenían división por cero casos. En una situación es manejado por un bloque try-catch, en la otra por un operador condicional. Aquí está mi tabla de resultados:

OS: Windows 8 6.2 x64
JVM: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 23.25-b01
Porcentaje | Resultado (prueba / if, ns)   
    0% | 88/90   
    1% | 89/87    
    10% | 86/97    
    90% | 85/83   

Lo que dice que no hay una diferencia significativa entre ninguno de estos casos.

Andrey Chaschev
fuente
3

He encontrado la captura de NullPointException bastante costosa. Para las operaciones de 1.2k, el tiempo fue de 200ms y 12ms cuando lo manejé de la misma manera, lo if(object==null)que fue una mejora considerable para mí.

Mateusz Kaflowski
fuente