Qual parte de lançar uma exceção é cara?

256

Em Java, usar throw / catch como parte da lógica quando não há realmente um erro geralmente é uma má ideia (em parte), porque lançar e capturar uma exceção é caro, e fazê-lo muitas vezes em um loop geralmente é muito mais lento do que outros estruturas de controle que não envolvem o lançamento de exceções.

Minha pergunta é: é o custo incorrido no lançamento / captura em si ou ao criar o objeto Exception (já que ele obtém muitas informações de tempo de execução, incluindo a pilha de execução)?

Em outras palavras, se eu fizer

Exception e = new Exception();

mas não jogue, isso representa a maior parte do custo de arremesso, ou o arremesso de arremesso + captura está custando caro?

Não estou perguntando se colocar código em um bloco try / catch aumenta o custo de executar esse código, estou perguntando se pegar a exceção é a parte cara ou criar (chamar o construtor) a exceção é a parte cara .

Outra maneira de perguntar isso é: se eu fizesse uma instância de Exception e a jogasse e pegasse repetidamente, isso seria significativamente mais rápido do que criar uma nova exceção toda vez que eu lançar?

Martin Carney
fonte
20
Acredito que esteja preenchendo e preenchendo o rastreamento de pilha.
Elliott Frisch
12
Verifique isto: stackoverflow.com/questions/16451777/…
Jorge
"se eu criei uma instância de Exception e a joguei e peguei repetidamente", quando a exceção é criada, o rastreamento de pilha é preenchido, o que significa que sempre será o mesmo stactrace, independentemente do local de onde foi lançado. Se o stacktrace não for importante para você, você pode tentar sua idéia, mas isso pode dificultar a depuração, se não impossível, em alguns casos.
Pshemo 31/03
2
@ Pshemo Não pretendo realmente fazer isso em código, estou perguntando sobre o desempenho e usando esse absurdo como um exemplo em que poderia fazer a diferença.
Martin Carney
@ MartinCarney Adicionei uma resposta ao seu último parágrafo, ou seja, o armazenamento em cache de uma exceção terá um ganho de desempenho. Se for útil, posso adicionar o código, caso contrário, posso excluir a resposta.
Harry

Respostas:

267

Criar um objeto de exceção não é mais caro do que criar outros objetos regulares. O custo principal está oculto no fillInStackTracemétodo nativo , que percorre a pilha de chamadas e coleta todas as informações necessárias para criar um rastreamento de pilha: classes, nomes de métodos, números de linhas etc.

O mito sobre altos custos de exceção vem do fato que a maioria dos Throwableconstrutores chama implicitamente fillInStackTrace. No entanto, há um construtor para criar um Throwablesem rastreamento de pilha. Ele permite que você jogue lançamentos muito rápidos para instanciar. Outra maneira de criar exceções leves é substituir fillInStackTrace.


Agora, que tal lançar uma exceção?
De fato, depende de onde uma exceção lançada é capturada .

Se for capturado no mesmo método (ou, mais precisamente, no mesmo contexto, já que o contexto pode incluir vários métodos devido a inlining), throwserá tão rápido e simples quanto goto(é claro, após a compilação do JIT).

No entanto, se um catchbloco estiver em algum lugar mais profundo da pilha, a JVM precisará desenrolar os quadros da pilha, e isso poderá demorar significativamente mais. Demora ainda mais, se houver synchronizedblocos ou métodos envolvidos, porque desenrolar implica na liberação de monitores pertencentes a quadros de pilha removidos.


Eu poderia confirmar as afirmações acima com benchmarks adequados, mas felizmente não preciso fazer isso, pois todos os aspectos já estão perfeitamente cobertos no post do engenheiro de desempenho do HotSpot Alexey Shipilev: O desempenho excepcional da exceção de Lil ' .

apangin
fonte
8
Conforme observado no artigo e abordado aqui, o resultado é que o custo de lançar / capturar exceções depende muito da profundidade das chamadas. O ponto aqui é que a declaração "exceções são caras" não está realmente correta. Uma afirmação mais correta é que as exceções 'podem' ser caras. Honestamente, acho que dizer apenas usar exceções para "casos verdadeiramente excepcionais" (como no artigo) é muito forte. Eles são perfeitos para praticamente qualquer coisa fora do fluxo de retorno normal e é difícil detectar o impacto no desempenho de usá-los dessa maneira em um aplicativo real.
JimmyJames
14
Pode valer a pena quantificar a sobrecarga das exceções. Mesmo no pior caso relatado neste artigo bastante exaustivo (lançar e capturar uma exceção dinâmica com um rastreamento de pilha que é realmente consultado, com 1000 quadros de pilha de profundidade), leva 80 microssegundos. Isso pode ser significativo se seu sistema precisar processar milhares de exceções por segundo, mas não vale a pena se preocupar. E esse é o pior caso; se seus rastreamentos de pilha forem um pouco mais saudáveis ​​ou você não os consultar, podemos processar quase um milhão de exceções por segundo.
meriton
13
Enfatizo isso porque muitas pessoas, ao lerem que as exceções são "caras", nunca param para perguntar "caras comparadas com o que", mas presumem que elas são "parte cara do programa", o que raramente são.
meriton
2
Há uma parte que não é mencionada aqui: o custo potencial para impedir a aplicação de otimizações. Um exemplo extremo seria a JVM que não faz inlining para evitar rastreamentos de pilha "confusos", mas vi (micro) benchmarks em que a presença ou ausência de exceções faria ou quebraria otimizações em C ++ antes.
Matthieu M.
3
@MatthieuM. Exceções e blocos try / catch não impedem a JVM de incluir. Para métodos compilados, os rastreamentos de pilha reais são reconstruídos a partir da tabela de quadros de pilha virtual armazenados como metadados. Não me lembro de uma otimização de JIT incompatível com try / catch. A estrutura try / catch em si não adiciona nada ao código do método, ela existe apenas como uma tabela de exceção, além do código.
apangin
72

A primeira operação na maioria dos Throwableconstrutores é preencher o rastreamento de pilha, que é onde está a maior parte da despesa.

Há, no entanto, um construtor protegido com um sinalizador para desativar o rastreamento de pilha. Esse construtor também é acessível ao estender Exception. Se você criar um tipo de exceção personalizado, poderá evitar a criação do rastreamento de pilha e obter um melhor desempenho à custa de menos informações.

Se você criar uma única exceção de qualquer tipo por meios normais, poderá repeti-la várias vezes sem a sobrecarga de preenchimento no rastreamento de pilha. No entanto, seu rastreamento de pilha refletirá onde foi construído, não onde foi lançado em uma instância específica.

As versões atuais do Java fazem algumas tentativas para otimizar a criação do rastreamento de pilha. O código nativo é chamado para preencher o rastreamento de pilha, que registra o rastreamento em uma estrutura nativa mais leve. Correspondentes Java StackTraceElementobjetos são preguiçosamente criado a partir desse registro apenas quando os getStackTrace(), printStackTrace()ou outros métodos que exigem o traço são chamados.

Se você eliminar a geração de rastreamento de pilha, o outro custo principal é desenrolar a pilha entre o arremesso e a captura. Quanto menos quadros intermediários forem encontrados antes da exceção ser capturada, mais rápido será.

Projete seu programa para que exceções sejam lançadas apenas em casos realmente excepcionais, e é difícil justificar otimizações como essas.

erickson
fonte
25

Há uma boa anotação sobre exceções aqui.

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

A conclusão é que a construção do traço da pilha e o desenrolamento da pilha são as peças caras. O código abaixo aproveita um recurso no 1.7qual podemos ativar e desativar os rastreamentos de pilha. Podemos então usar isso para ver que tipo de custo os diferentes cenários têm

A seguir, são mostrados os horários apenas para a criação de objetos. Eu adicionei Stringaqui para que você possa ver que, sem a pilha sendo escrita, quase não há diferença na criação de um JavaExceptionObjeto e um String. Com a escrita de pilha ativada, a diferença é dramática, ou seja, pelo menos uma ordem de magnitude mais 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 seguir, mostra quanto tempo levou para retornar de um arremesso a uma profundidade específica um milhão de vezes.

|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 (%)| 

O seguinte é quase certamente uma simplificação bruta ...

Se tivermos uma profundidade de 16 com a gravação de pilha ativada, a criação de objetos levará aproximadamente ~ 40% do tempo, o rastreamento de pilha real será responsável pela grande maioria disso. ~ 93% da instanciação do objeto JavaException se deve ao rastreamento da pilha que está sendo realizado. Isso significa que o desenrolar da pilha nesse caso está demorando os outros 50% do tempo.

Quando desligamos a criação de objetos de rastreamento de pilha, é responsável por uma fração muito menor, ou seja, 20%, e o desenrolamento de pilha agora responde por 80% do tempo.

Nos dois casos, o desenrolamento da pilha leva uma grande parte do tempo 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];
  };
}

Os quadros de pilha neste exemplo são pequenos em comparação com o que você normalmente encontraria.

Você pode espiar o bytecode usando javap

javap -c -v -constants JavaException.class

isto é, para o 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
atormentar
fonte
13

A criação do rastreio Exceptioncom uma nullpilha leva tanto tempo quanto o bloco throwe try-catchjuntos. No entanto, o preenchimento do rastreamento da pilha leva em média 5x mais tempo .

Criei a seguinte referência para demonstrar o impacto no desempenho. Eu adicionei o -Djava.compiler=NONEà configuração de execução para desativar a otimização do compilador. Para medir o impacto da criação do rastreamento de pilha, estendi a Exceptionclasse para aproveitar o construtor sem pilha:

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

O código de referência é o seguinte:

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

}

Resultado:

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

Isso significa que criar um NoStackExceptioné aproximadamente tão caro quanto jogar repetidamente o mesmo Exception. Também mostra que a criação Exceptione o preenchimento de um rastreamento de pilha levam aproximadamente quatro vezes mais.

Austin D
fonte
1
Você pode adicionar mais um caso em que você cria uma instância de Exception antes do horário de início e, em seguida, joga + captura repetidamente em um loop? Isso mostraria o custo de apenas jogar + pegar.
Martin Carney
@MartinCarney Ótima sugestão! Eu atualizei minha resposta para fazer exatamente isso.
Austin D
Eu fiz alguns ajustes no seu código de teste, e parece que o compilador está fazendo alguma otimização que nos impede de obter números precisos.
Martin Carney
@MartinCarney Atualizei a resposta para a otimização do compilador de descontos
Austin D
Para sua informação, você provavelmente deve ler as respostas de Como eu escrevo um micro-benchmark correto em Java? Dica: não é isso.
Daniel Pryden
4

Esta parte da pergunta ...

Outra maneira de perguntar isso é: se eu fizesse uma instância de Exception e a jogasse e pegasse repetidamente, isso seria significativamente mais rápido do que criar uma nova exceção toda vez que eu lançar?

Parece perguntar se a criação de uma exceção e o armazenamento em cache em algum lugar melhora o desempenho. Sim. É o mesmo que desativar a pilha que está sendo gravada na criação do objeto, porque já foi feita.

Estes são os horários que recebi, leia a advertência depois disso ...

|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 (%)| 

É claro que o problema com isso é o rastreamento da pilha agora aponta para onde você instancia o objeto e não para onde foi lançado.

atormentar
fonte
3

Usando a resposta do @ AustinD como ponto de partida, fiz alguns ajustes. Código na parte inferior.

Além de adicionar o caso em que uma instância de exceção é lançada repetidamente, também desativei a otimização do compilador para que possamos obter resultados precisos de desempenho. Eu adicionei -Djava.compiler=NONEaos argumentos da VM, conforme esta resposta . (No eclipse, edite o Run Configuration → Arguments para configurar este argumento da VM)

Os resultados:

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

Portanto, criar a exceção custa cerca de cinco vezes mais do que jogar + pegá-la. Supondo que o compilador não otimize muito o custo.

Para comparação, aqui está o mesmo teste sem desativar a otimização:

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
fonte
Desabilitando a otimização = ótima técnica! Vou editar a minha resposta original, de modo a não ninguém enganar
Austin D
3
Desabilitar a otimização não é melhor do que escrever um benchmark defeituoso, pois o modo puro interpretado não tem nada a ver com o desempenho do mundo real. O poder da JVM é o compilador JIT, então qual é o objetivo de medir algo que não reflete como o aplicativo real funciona?
31516 apangin
2
Existem muito mais aspectos na criação, lançamento e captura de exceções do que os convertidos neste 'benchmark'. Eu sugiro fortemente que você leia este post .
apangin 31/03