É caro usar blocos try-catch, mesmo que uma exceção nunca seja lançada?

189

Sabemos que é caro capturar exceções. Mas, também é caro usar um bloco try-catch em Java, mesmo que uma exceção nunca seja lançada?

Encontrei a pergunta / resposta do estouro de pilha Por que os blocos de tentativa são caros? , mas é para .NET .

jsedano
fonte
30
Realmente não há razão para esta pergunta. Try..catch tem uma finalidade muito específica. Se você precisar, precisa. De qualquer forma, que ponto é uma tentativa sem uma pegadinha?
JohnFx
49
try { /* do stuff */ } finally { /* make sure to release resources */ }é legal e útil
A4L 08/08
4
Esse custo deve ser ponderado em relação aos benefícios. Não fica sozinho. De qualquer forma, caro é relativo e, até que você saiba que não pode fazê-lo, faz sentido usar o método mais óbvio, em vez de não fazer algo, pois isso pode economizar um milissegundo ou dois ao longo de uma hora de duração. execução de programa.
Joel
4
Espero que isso não é uma vantagem para um "vamos-reinventar-códigos de erro" tipo de situação ...
mikołak
6
@SAFX: com Java7, você pode até se livrar do finallybloco usando atry-with-resources
a_horse_with_no_name

Respostas:

201

tryquase não tem nenhuma despesa. Em vez de fazer o trabalho de configurar o trytempo de execução, os metadados do código são estruturados no tempo de compilação, de modo que, quando uma exceção é lançada, ele agora faz uma operação relativamente cara, subindo a pilha e ver se tryexistem blocos que capturariam isso. exceção. Do ponto de vista de um leigo, também trypode ser livre. Na verdade, está lançando a exceção que lhe custa - mas, a menos que você esteja lançando centenas ou milhares de exceções, você ainda não perceberá o custo.


trytem alguns custos menores associados a ele. Java não pode fazer algumas otimizações no código em um trybloco que faria de outra forma. Por exemplo, o Java geralmente reorganiza as instruções em um método para torná-lo mais rápido - mas o Java também precisa garantir que, se uma exceção for lançada, a execução do método seja observada como se suas instruções, como escritas no código-fonte, fossem executadas em ordem até alguma linha.

Como em um trybloco, uma exceção pode ser lançada (em qualquer linha do bloco try! Algumas exceções são lançadas de forma assíncrona, como chamar stopum Thread (que foi descontinuado) e, além disso, o OutOfMemoryError pode acontecer em quase qualquer lugar), e ainda assim pode ser capturado e o código continua sendo executado posteriormente no mesmo método, é mais difícil pensar em otimizações que podem ser feitas, portanto, é menos provável que isso aconteça. (Alguém precisaria programar o compilador para executá-los, argumentar e garantir a correção, etc. Seria uma grande dor para algo que deveria ser 'excepcional'). Mas, novamente, na prática, você não notará coisas assim.

Patashu
fonte
2
Algumas exceções são lançadas de forma assíncrona , elas não são assíncronas, mas lançadas em pontos seguros. e esta parte da tentativa tem alguns custos menores associados a ela. Java não pode fazer algumas otimizações no código em um bloco try que, caso contrário , precisaria de uma referência séria. Em algum momento, é muito provável que o código esteja dentro do bloco try / catch. Pode ser verdade que seria mais difícil alinhar o bloco try / catch e criar uma estrutura adequada para o resultado, mas a parte com reorganização é ambígua.
bestsss 23/05
2
Um try...finallybloco sem catchtambém impede algumas otimizações?
dajood
5
@ Patashu "Na verdade, está lançando a exceção que custa" Tecnicamente, lançar a exceção não é caro; instanciar o Exceptionobjeto é o que leva a maior parte do tempo.
Austin D
72

Vamos medir, sim?

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);
        }
    }
}

No meu computador, isso imprime algo como:

try     0.598 ns
no try  0.601 ns

Pelo menos neste exemplo trivial, a instrução try não teve impacto mensurável no desempenho. Sinta-se livre para medir os mais complexos.

De um modo geral, recomendo não se preocupar com o custo de desempenho das construções de linguagem até que você tenha evidências de um problema real de desempenho em seu código. Ou, como Donald Knuth colocou : "a otimização prematura é a raiz de todo mal".

Meriton
fonte
4
Embora seja provável que a tentativa / não tentativa seja a mesma na maioria das JVM, a marca microbench é terrivelmente falha.
bestsss 23/05
2
alguns níveis: você quer dizer que os resultados são calculados em menos de 1ns? O código compilado removerá o try / catch AND o loop completamente (o número de soma de 1 a n é uma soma de progressão aritmética trivial). Mesmo que o código contenha try /, finalmente, o compilador pode provar, não há nada a ser jogado lá. O código do resumo possui apenas 2 sites de chamada e será clonado e incorporado. Existem mais casos, basta procurar alguns artigos sobre a marca de microbench e você decide escrever uma marca de microbench sempre verificar o conjunto gerado.
bestsss 24/05
3
Os tempos relatados são por iteração do loop. Como uma medida só será usada se tiver um tempo total decorrido> 0,1 segundos (ou 2 bilhões de iterações, o que não foi o caso aqui), acho sua afirmação de que o loop foi removido na íntegra é difícil de acreditar - porque se o loop foi removido, o que levou 0,1 segundos para executar?
meriton 24/05
... e, de fato, de acordo com -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly, o loop e a adição estão presentes no código nativo gerado. E não, os métodos abstratos não são incorporados, porque o chamador não é apenas compilado no tempo (presumivelmente, porque não é chamado o suficiente).
meriton 24/05
Como faço para escrever uma micro-referência correto em Java: stackoverflow.com/questions/504103/...
Vadzim
46

try/ catchpode ter algum impacto no desempenho. Isso ocorre porque impede que a JVM faça algumas otimizações. Joshua Bloch, em "Java Efetivo", disse o seguinte:

• A colocação do código dentro de um bloco try-catch inibe certas otimizações que as implementações modernas da JVM poderiam executar.

Evgeniy Dorofeev
fonte
24
"impede que a JVM faça algumas otimizações" ...? Você poderia elaborar alguma coisa?
The Kraken
5
@ O código Kraken dentro dos blocos try (geralmente? Sempre?) Não pode ser reordenado com o código fora dos blocos try, como um exemplo.
Patashu
3
Observe que a pergunta era se "é caro", não se "tem algum impacto no desempenho".
mikołak
3
adicionou um trecho do Effective Java , e essa é a bíblia do java, é claro; a menos que haja uma referência, o trecho não diz nada. Praticamente qualquer código em java está dentro de try / finalmente em algum nível.
bestsss 23/05
29

Sim, como os outros disseram, um trybloco inibe algumas otimizações entre os {}personagens que o cercam. Em particular, o otimizador deve assumir que uma exceção pode ocorrer em qualquer ponto do bloco, portanto, não há garantia de que as instruções sejam executadas.

Por exemplo:

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

Sem o try, o valor calculado para atribuir a xpode ser salvo como uma "subexpressão comum" e reutilizado para atribuir a y. Mas por causa dotry não há garantia de que a primeira expressão tenha sido avaliada, a expressão deve ser recalculada. Normalmente, isso não é um grande problema no código "linear", mas pode ser significativo em um loop.

Note-se, no entanto, que isso se aplica SOMENTE ao código JITCed. O javac faz apenas uma pequena quantidade de otimização e o custo do interpretador de bytecodes é zero para entrar / sair de um trybloco. (Não há bytecodes gerados para marcar os limites do bloco.)

E para bestsss:

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

Resultado:

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

saída 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
}

Não "GOTO".

Hot Licks
fonte
Não há bytecodes gerados para marcar os limites do bloco, isso não é necessariamente - ele exige que o GOTO deixe o bloco, caso contrário, ele cairá no catch/finallyquadro.
bestsss 24/05
@bestsss - Mesmo que um GOTO seja gerado (o que não é um dado), o custo é minúsculo e está longe de ser um "marcador" para um limite de bloco - o GOTO pode ser gerado para muitas construções.
Hot Licks
Eu nunca mencionei o custo, no entanto, não há bytecodes gerados é uma declaração falsa. Isso é tudo. Na verdade, não há blocos no bytecode, os quadros não são iguais aos blocos.
bestsss 24/05
Não haverá um GOTO se a tentativa cair diretamente no finalmente, e há outros cenários em que não haverá GOTO. O ponto é que não há nada na ordem dos bytecodes "enter try" / "exit try".
Hot Licks
Não haverá um GOTO se a tentativa cair diretamente no finalmente - False! não existe finallyno bytecode, é try/catch(Throwable any){...; throw any;}E tem uma instrução catch w / a frame e Throwable que DEVE SER definida (não nula) e assim por diante. Por que você tenta discutir sobre o tópico, pode verificar pelo menos alguns códigos de bytes? A diretriz atual para impl. Finalmente, copie os blocos e evite a seção goto (impl anterior), mas os bytecodes precisam ser copiados dependendo de quantos pontos de saída existem.
bestsss 24/05
8

Para entender por que as otimizações não podem ser executadas, é útil entender os mecanismos subjacentes. O exemplo mais sucinto que pude encontrar foi implementado nas macros C em: 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)

Os compiladores geralmente têm dificuldade em determinar se um salto pode ser localizado em X, Y e Z, para ignorar otimizações que não podem garantir a segurança, mas a implementação em si é bastante leve.

technosaurus
fonte
4
Essas macros C que você encontrou para try / catch não são equivalentes à implementação Java ou C #, que emitem 0 instruções de tempo de execução.
Patashu
A implementação do java é muito extensa para incluir na íntegra; é uma implementação simplificada com o objetivo de entender a idéia básica de como as exceções podem ser implementadas. Dizer que emite 0 instruções de tempo de execução é enganoso. Por exemplo, uma simples classcastexception estende runtimeexception que estende a exceção que estende jogável que envolve: grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ... É como dizer que um caso de mudança em C é grátis se apenas um caso for usado, ainda haverá uma pequena sobrecarga de inicialização.
Technosaurus # 10/13
1
@ Patashu Todos esses bits pré-compilados ainda precisam ser carregados na inicialização, sejam eles usados ​​ou não. Não há como saber se haverá uma exceção de falta de memória durante o tempo de execução no tempo de compilação - é por isso que eles são chamados de exceções de tempo de execução - caso contrário, seriam avisos / erros do compilador, portanto não otimiza tudo , todo o código para manipulá-los é incluído no código compilado e tem um custo de inicialização.
Technosaurus # 10/13
2
Não posso falar sobre C. Em C # e Java, a tentativa é implementada adicionando metadados, não código. Quando um bloco try é inserido, nada é executado para indicar isso - quando uma exceção é lançada, a pilha é desenrolada e os metadados são verificados para manipuladores desse tipo de exceção (caro).
Patashu
1
Sim, na verdade eu implementei um interpretador Java e um compilador de bytecode estático e trabalhei em um JITC subsequente (para IBM iSeries) e posso dizer que não há nada que "marque" a entrada / saída do intervalo de tentativa nos bytecodes, mas sim os intervalos são identificados em uma tabela separada. O intérprete não faz nada de especial para um intervalo de tentativa (até que uma exceção seja gerada). Um JITC (ou compilador de bytecode estático) deve estar ciente dos limites para suprimir as otimizações, conforme indicado anteriormente.
Hot Licks
8

Mais uma marca de microbench ( fonte ).

Criei um teste no qual medi a versão do código try-catch e no-try-catch com base em uma porcentagem de exceção. A porcentagem de 10% significa que 10% dos casos de teste tiveram divisão por zero casos. Em uma situação, ele é tratado por um bloco try-catch, na outra por um operador condicional. Aqui está a minha tabela de resultados:

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

O que diz que não há diferença significativa entre nenhum desses casos.

Andrey Chaschev
fonte
3

Eu achei a captura do NullPointException bastante cara. Para operações de 1.2k, o tempo era de 200ms e 12ms quando eu o manipulava da mesma maneira com a if(object==null)qual foi bastante aprimorado para mim.

Mateusz Kaflowski
fonte