java - tipos - usando o try catch




Quão lentas são as exceções Java? (12)

Aleksey Shipilëv fez uma análise muito minuciosa na qual ele faz referência às exceções Java sob várias combinações de condições:

  • Exceções recém-criadas versus exceções pré-criadas
  • Rastreio de pilha ativado vs desativado
  • Rastreio de pilha solicitado vs nunca solicitado
  • Apanhado no nível superior vs retraído em todos os níveis vs encadeado / envolvido em todos os níveis
  • Vários níveis de profundidade da pilha de chamadas Java
  • Sem otimizações inline vs inlining extremo vs configurações padrão
  • Campos definidos pelo usuário lidos vs não lidos

Ele também os compara com o desempenho de verificar um código de erro em vários níveis de frequência de erro.

As conclusões (citadas textualmente de seu post) foram:

  1. Exceções verdadeiramente excepcionais são maravilhosamente eficientes. Se você usá-los como projetado, e só comunicar os casos realmente excepcionais entre o número esmagadoramente grande de casos não excepcionais manipulados pelo código regular, usar exceções é a vitória do desempenho.

  2. Os custos de desempenho das exceções têm dois componentes principais: construção de rastreamento de pilha quando Exceção é instanciada e desenrolamento de pilha durante o lançamento de Exceção.

  3. Os custos de construção de rastreamento de pilha são proporcionais à profundidade da pilha no momento da instanciação de exceção. Isso já é ruim porque quem na Terra conhece a profundidade da pilha na qual esse método de lançamento seria chamado? Mesmo se você desativar a geração de rastreio de pilha e / ou armazenar em cache as exceções, você só poderá se livrar dessa parte do custo de desempenho.

  4. Os custos de desenrolamento da pilha dependem da sorte que temos ao aproximar o manipulador de exceções no código compilado. A estruturação cuidadosa do código para evitar a pesquisa profunda de manipuladores de exceção provavelmente está nos ajudando a ter mais sorte.

  5. Devemos eliminar os dois efeitos, o custo de desempenho das exceções é o do ramo local. Não importa o quão bonito pareça, isso não significa que você deve usar Exceptions como o fluxo de controle usual, porque nesse caso você está à mercê de otimizar o compilador! Você só deve usá-los em casos realmente excepcionais, em que a freqüência de exceção amortiza o possível custo incorreto de aumentar a exceção real.

  6. A regra otimista parece ser 10 ^ -4 de freqüência para exceções é excepcional o suficiente. Isso, claro, depende dos pesos pesados ​​das próprias exceções, das ações exatas tomadas nos manipuladores de exceção, etc.

O resultado é que, quando uma exceção não é lançada, você não paga um custo, portanto, quando a condição excepcional é suficientemente rara, o tratamento de exceções é mais rápido do que o uso de if todas as vezes. O post completo vale muito a pena ler.

Pergunta: O tratamento de exceções em Java é realmente lento?

A sabedoria convencional, bem como muitos resultados do Google, afirma que a lógica excepcional não deve ser usada para o fluxo normal de programas em Java. Duas razões são geralmente dadas,

  1. é realmente lento - até mesmo uma ordem de magnitude mais lenta que o código normal (as razões dadas variam)

e

  1. é confuso porque as pessoas esperam que apenas erros sejam tratados em código excepcional.

Esta questão é sobre o # 1.

Como exemplo, esta página descreve o tratamento de exceções Java como "muito lento" e relaciona a lentidão à criação da cadeia de mensagens de exceção - "essa cadeia é usada na criação do objeto de exceção que é gerado. Isso não é rápido". O artigo Tratamento de Exceção Efetiva em Java diz que "a razão para isso é devido ao aspecto de criação de objeto do tratamento de exceções, que torna as exceções inerentemente lentas". Outra razão é que a geração de rastreamento de pilha é o que diminui a velocidade.

Meu teste (usando Java 1.6.0_07, Java HotSpot 10.0, no Linux de 32 bits), indica que o tratamento de exceção não é mais lento que o código normal. Eu tentei executar um método em um loop que executa algum código. No final do método, eu uso um booleano para indicar se devo retornar ou jogar . Desta forma, o processamento real é o mesmo. Eu tentei executar os métodos em ordens diferentes e calcular a média dos tempos de teste, pensando que talvez fosse o aquecimento da JVM. Em todos os meus testes, o lançamento foi pelo menos tão rápido quanto o retorno, se não mais rápido (até 3,1% mais rápido). Estou completamente aberto à possibilidade de que meus testes estivessem errados, mas não vi nada no caminho do exemplo de código, comparações de teste ou resultados no último ano ou dois que mostrem manipulação de exceção em Java para realmente lento.

O que me levou a esse caminho foi uma API que eu precisava usar e que lançava exceções como parte da lógica de controle normal. Eu queria corrigi-los em seu uso, mas agora posso não conseguir. Será que vou ter que elogiá-los em seu pensamento futuro?

No documento Manuseio de exceção Java eficiente na compilação just-in-time , os autores sugerem que a presença de manipuladores de exceção, mesmo se nenhuma exceção for lançada, é suficiente para impedir que o compilador JIT otimize o código corretamente, diminuindo a velocidade . Eu não testei essa teoria ainda.


Depende de como as exceções são implementadas. A maneira mais simples é usar setjmp e longjmp. Isso significa que todos os registradores da CPU são gravados na pilha (o que já leva algum tempo) e possivelmente alguns outros dados precisam ser criados ... tudo isso já acontece na instrução try. A instrução throw precisa desenrolar a pilha e restaurar os valores de todos os registradores (e possíveis outros valores na VM). Portanto, tentar e lançar são igualmente lentos, e isso é muito lento, mas se nenhuma exceção for lançada, sair do bloco try não leva tempo na maioria dos casos (já que tudo é colocado na pilha, que limpa automaticamente se o método existir).

A Sun e outros reconheceram que isso é possivelmente abaixo do ideal e, é claro, as VMs ficam cada vez mais rápidas com o passar do tempo. Há outra maneira de implementar exceções, o que faz com que a própria tentativa seja muito rápida (na verdade, nada acontece para tentar de maneira geral - tudo o que precisa acontecer já está feito quando a classe é carregada pela VM) e não . Eu não sei qual JVM usa essa nova e melhor técnica ...

... mas você está escrevendo em Java para que seu código seja executado apenas em uma JVM em um sistema específico? Já que, se alguma vez for executado em qualquer outra plataforma ou qualquer outra versão da JVM (possivelmente de qualquer outro fornecedor), quem diz que também usa a implementação rápida? O mais rápido é mais complicado que o lento e não é facilmente possível em todos os sistemas. Você quer ficar portável? Então não confie nas exceções sendo rápidas.

Também faz uma grande diferença o que você faz dentro de um bloco try. Se você abrir um bloco try e nunca chamar nenhum método dentro deste bloco try, o bloco try será ultra rápido, já que o JIT pode então tratar um lance como um simples goto. Ele não precisa salvar o estado da pilha nem precisa desenrolar a pilha se uma exceção for lançada (ela só precisa saltar para os manipuladores de captura). No entanto, isso não é o que você costuma fazer. Normalmente você abre um bloco try e então chama um método que pode lançar uma exceção, certo? E mesmo que você apenas use o bloco try dentro do seu método, que tipo de método será esse, que não chama nenhum outro método? Será que vai calcular um número? Então, para que você precisa de exceções? Existem maneiras muito mais elegantes de regular o fluxo do programa. Para praticamente qualquer outra coisa além de matemática simples, você terá que chamar um método externo e isso já destrói a vantagem de um bloco try local.

Veja o seguinte código de teste:

public class Test {
    int value;


    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    // Calculates without exception
    public void method1(int i) {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            System.out.println("You'll never see this!");
        }
    }

    // Could in theory throw one, but never will
    public void method2(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // Will never be true
        if ((i & 0xFFFFFFF) == 1000000000) {
            throw new Exception();
        }
    }

    // This one will regularly throw one
    public void method3(int i) throws Exception {
        value = ((value + i) / i) << 1;
        // i & 1 is equally fast to calculate as i & 0xFFFFFFF; it is both
        // an AND operation between two integers. The size of the number plays
        // no role. AND on 32 BIT always ANDs all 32 bits
        if ((i & 0x1) == 1) {
            throw new Exception();
        }
    }

    public static void main(String[] args) {
        int i;
        long l;
        Test t = new Test();

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            t.method1(i);
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method1 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method2(i);
            } catch (Exception e) {
                System.out.println("You'll never see this!");
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method2 took " + l + " ms, result was " + t.getValue()
        );

        l = System.currentTimeMillis();
        t.reset();
        for (i = 1; i < 100000000; i++) {
            try {
                t.method3(i);
            } catch (Exception e) {
                // Do nothing here, as we will get here
            }
        }
        l = System.currentTimeMillis() - l;
        System.out.println(
            "method3 took " + l + " ms, result was " + t.getValue()
        );
    }
}

Resultado:

method1 took 972 ms, result was 2
method2 took 1003 ms, result was 2
method3 took 66716 ms, result was 2

A lentidão do bloco try é muito pequena para descartar fatores de confusão, como processos em segundo plano. Mas o bloco catch matou tudo e chegou 66 vezes mais devagar!

Como eu disse, o resultado não será tão ruim se você colocar try / catch e jogar tudo dentro do mesmo método (method3), mas esta é uma otimização JIT especial na qual eu não confiaria. E mesmo usando esta otimização, o lançamento ainda é bem lento. Então eu não sei o que você está tentando fazer aqui, mas definitivamente há uma maneira melhor de fazer isso do que usar try / catch / throw.


Eu acho que o primeiro artigo se refere ao ato de percorrer a pilha de chamadas e criar um rastreamento de pilha como sendo a parte cara, e enquanto o segundo artigo não diz isso, eu acho que é a parte mais cara da criação de objetos. John Rose tem um artigo onde ele descreve diferentes técnicas para acelerar as exceções . (Pré-alocação e reutilização de uma exceção, exceções sem rastreamentos de pilha, etc)

Mas ainda assim - acho que isso deve ser considerado apenas um mal necessário, um último recurso. A razão de John para fazer isso é emular recursos em outras linguagens que não estão (ainda) disponíveis na JVM. Você NÃO deve adquirir o hábito de usar exceções para o fluxo de controle. Especialmente não por razões de desempenho! Como você mesmo mencionou no # 2, corre o risco de mascarar erros graves em seu código dessa maneira, e será mais difícil de manter para novos programadores.

Microbenchmarks em Java são surpreendentemente difíceis de acertar (eu tenho dito), especialmente quando você entra em território JIT, então eu realmente duvido que usar exceções é mais rápido do que "retornar" na vida real. Por exemplo, eu suspeito que você tenha algo entre 2 e 5 quadros de pilha no seu teste? Agora imagine que seu código será invocado por um componente JSF implementado pelo JBoss. Agora você pode ter um rastreamento de pilha com várias páginas.

Talvez você possa postar seu código de teste?


Fiz alguns testes de desempenho com a JVM 1.5 e o uso de exceções foi pelo menos 2x mais lento. Em média: Tempo de execução em um método trivialmente pequeno mais que triplicado (3x) com exceções. Um loop trivialmente pequeno que teve que pegar a exceção viu um aumento de 2x no tempo próprio.

Eu vi números semelhantes no código de produção, bem como micro benchmarks.

Exceções definitivamente não devem ser usadas para qualquer coisa que seja chamada com freqüência. Lançar milhares de exceções por segundo causaria um enorme gargalo.

Por exemplo, usando "Integer.ParseInt (...)" para encontrar todos os valores ruins em um arquivo de texto muito grande - idéia muito ruim. (Eu vi esse método utilitário matar o desempenho no código de produção)

Usando uma exceção para relatar um valor inválido em um formulário de GUI do usuário, provavelmente não é tão ruim do ponto de vista do desempenho.

Seja ou não uma boa prática de design, eu usaria a regra: se o erro é normal / esperado, use um valor de retorno. Se for anormal, use uma exceção. Por exemplo: lendo entradas do usuário, valores ruins são normais - use um código de erro. Passando um valor para uma função de utilidade interna, os valores ruins devem ser filtrados pelo código de chamada - use uma exceção.


Minha resposta, infelizmente, é muito longa para postar aqui. Então deixe-me resumir aqui e encaminhá-lo para http://www.fuwjax.com/how-slow-are-java-exceptions/ para os detalhes corajosos.

A questão real aqui não é "Quão lentos são os 'fracassos relatados como exceções' em comparação com o 'código que nunca falha'?" como a resposta aceita pode fazer você acreditar. Em vez disso, a pergunta deve ser "Quão lentos são os 'erros relatados como exceções' em comparação com falhas relatadas de outras maneiras?" Geralmente, as outras duas maneiras de relatar falhas são com valores de sentinela ou com wrappers de resultado.

Os valores do Sentinel são uma tentativa de retornar uma classe no caso de sucesso e outra no caso de falha. Você pode pensar nisso quase como retornar uma exceção em vez de jogar uma. Isso requer uma classe pai compartilhada com o objeto de sucesso e, em seguida, faz uma verificação de "instanceof" e um par é acionado para obter as informações de sucesso ou falha.

Acontece que, sob o risco da segurança do tipo, os valores do Sentinel são mais rápidos que as exceções, mas apenas por um fator de aproximadamente 2x. Agora, isso pode parecer muito, mas esse 2x cobre apenas o custo da diferença de implementação. Na prática, o fator é muito menor, já que nossos métodos que podem falhar são muito mais interessantes do que alguns operadores aritméticos, como no exemplo de código em outra parte desta página.

Os Wrappers de resultados, por outro lado, não sacrificam a segurança do tipo. Eles envolvem as informações de sucesso e falha em uma única classe. Então, em vez de "instanceof", eles fornecem um "isSuccess ()" e getters para os objetos de sucesso e falha. No entanto, os objetos de resultado são aproximadamente 2x mais lentos do que usando exceções. Acontece que criar um novo objeto wrapper a cada vez é muito mais caro do que lançar uma exceção às vezes.

Além disso, as exceções são a linguagem fornecida para indicar que um método pode falhar. Não há outra maneira de dizer apenas pela API quais métodos devem sempre funcionar (principalmente) e quais devem relatar falhas.

As exceções são mais seguras do que as sentinelas, mais rápidas que os objetos resultantes e menos surpreendentes do que as outras. Não estou sugerindo que tente / catch replace if / else, mas as exceções são o caminho certo para relatar falhas, mesmo na lógica de negócios.

Dito isso, gostaria de salientar que as duas formas mais frequentes de afetar substancialmente o desempenho que encontrei estão criando objetos desnecessários e loops aninhados. Se você tiver uma escolha entre criar uma exceção ou não criar uma exceção, não crie a exceção. Se você tiver uma escolha entre criar uma exceção às vezes ou criar outro objeto o tempo todo, crie a exceção.


Não sei se esses tópicos se relacionam, mas uma vez eu quis implementar um truque baseado no rastreamento de pilha do thread atual: Eu queria descobrir o nome do método, que acionou a instanciação dentro da classe instanciada (sim, a ideia é louca, Eu desisti totalmente). Então eu descobri que chamar Thread.currentThread().getStackTrace() é extremamente lento (devido ao método dumpThreads nativo que ele usa internamente).

Então, Java Throwable , correspondentemente, tem um método nativo fillInStackTrace . Eu acho que o bloco killer- catch descrito anteriormente de alguma forma aciona a execução desse método.

Mas deixe-me contar uma outra história ...

No Scala, alguns recursos funcionais são compilados na JVM usando o ControlThrowable , que estende o Throwable e substitui o fillInStackTrace da seguinte maneira:

override def fillInStackTrace(): Throwable = this

Então eu adaptei o teste acima (quantidade de ciclos são diminuídos por dez, minha máquina é um pouco mais lenta :):

class ControlException extends ControlThrowable

class T {
  var value = 0

  def reset = {
    value = 0
  }

  def method1(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      println("You'll never see this!")
    }
  }

  def method2(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0xfffffff) == 1000000000) {
      throw new Exception()
    }
  }

  def method3(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new Exception()
    }
  }

  def method4(i: Int) = {
    value = ((value + i) / i) << 1
    if ((i & 0x1) == 1) {
      throw new ControlException()
    }
  }
}

class Main {
  var l = System.currentTimeMillis
  val t = new T
  for (i <- 1 to 10000000)
    t.method1(i)
  l = System.currentTimeMillis - l
  println("method1 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method2(i)
  } catch {
    case _ => println("You'll never see this")
  }
  l = System.currentTimeMillis - l
  println("method2 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method4(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method4 took " + l + " ms, result was " + t.value)

  t.reset
  l = System.currentTimeMillis
  for (i <- 1 to 10000000) try {
    t.method3(i)
  } catch {
    case _ => // do nothing
  }
  l = System.currentTimeMillis - l
  println("method3 took " + l + " ms, result was " + t.value)

}

Então, os resultados são:

method1 took 146 ms, result was 2
method2 took 159 ms, result was 2
method4 took 1551 ms, result was 2
method3 took 42492 ms, result was 2

Você vê, a única diferença entre method3 e method4 é que eles lançam diferentes tipos de exceções. Sim, o method4 ainda é mais lento que o method1 e o method2 , mas a diferença é muito mais aceitável.


Um tempo atrás eu escrevi uma classe para testar o desempenho relativo de converter strings para ints usando duas abordagens: (1) chamar Integer.parseInt () e pegar a exceção, ou (2) corresponder a string com um regex e chamar parseInt () somente se a partida for bem sucedida. Eu usei o regex da maneira mais eficiente que pude (ou seja, criando os objetos Pattern e Matcher antes de interligar o loop), e não imprimi ou salvei os stacktraces das exceções.

Para uma lista de dez mil strings, se todos fossem números válidos, a abordagem parseInt () era quatro vezes mais rápida que a abordagem regex. Mas se apenas 80% das cadeias fossem válidas, o regex era duas vezes mais rápido que parseInt (). E se 20% fossem válidos, significando que a exceção foi lançada e capturada em 80% do tempo, o regex foi cerca de vinte vezes mais rápido que parseInt ().

Fiquei surpreso com o resultado, considerando que a abordagem regex processa cadeias válidas duas vezes: uma vez para a correspondência e novamente para parseInt (). Mas jogando e pegando exceções mais do que compensado por isso. Esse tipo de situação provavelmente não ocorrerá com muita frequência no mundo real, mas, se isso acontecer, você definitivamente não deve usar a técnica de captura de exceção. Mas se você está apenas validando a entrada do usuário ou algo assim, use a abordagem parseInt ().


Basta comparar digamos Integer.parseInt ao método a seguir, que apenas retorna um valor padrão no caso de dados não mensuráveis ​​em vez de lançar uma exceção:

  public static int parseUnsignedInt(String s, int defaultValue) {
    final int strLength = s.length();
    if (strLength == 0)
      return defaultValue;
    int value = 0;
    for (int i=strLength-1; i>=0; i--) {
      int c = s.charAt(i);
      if (c > 47 && c < 58) {
        c -= 48;
        for (int j=strLength-i; j!=1; j--)
          c *= 10;
        value += c;
      } else {
        return defaultValue;
      }
    }
    return value < 0 ? /* übergebener wert > Integer.MAX_VALUE? */ defaultValue : value;
  }

Contanto que você aplique os dois métodos aos dados "válidos", ambos funcionarão aproximadamente na mesma taxa (mesmo que Integer.parseInt consiga manipular dados mais complexos). Mas assim que você tentar analisar dados inválidos (por exemplo, para analisar "abc" 1.000.000 vezes), a diferença no desempenho deve ser essencial.


Minha opinião sobre a velocidade de exceção versus a verificação de dados programaticamente.

Muitas classes tinham o conversor de String para valor (scanner / analisador), bibliotecas respeitadas e conhecidas também;)

geralmente tem forma

class Example {
public static Example Parse(String input) throws AnyRuntimeParsigException
...
}

nome da exceção é apenas um exemplo, geralmente é desmarcada (runtime), então lança declaração é apenas minha foto

às vezes existe segunda forma:

public static Example Parse(String input, Example defaultValue)

nunca jogando

Quando o segundo não estiver disponível (ou o programador ler muito menos documentos e usar somente primeiro), escreva esse código com expressão regular. A expressão regular é legal, politicamente correta, etc:

Xxxxx.regex(".....pattern", src);
if(ImTotallySure)
{
  Example v = Example.Parse(src);
}

com este programador de códigos não tem custo de exceções. Mas tem custo muito alto comparável de expressões regulares SEMPRE contra o pequeno custo de exceção, às vezes.

Eu uso quase sempre nesse contexto

try { parse } catch(ParsingException ) // concrete exception from javadoc
{
}

sem analisar o stacktrace etc, acredito que depois de palestras do seu bastante velocidade.

Não tenha medo Exceções


Por que exceções devem ser mais lentas que retornos normais?

Contanto que você não imprima o stacktrace no terminal, salve-o em um arquivo ou algo semelhante, o bloco catch não faz mais nenhum trabalho do que outros blocos de código. Então, eu não posso imaginar porque "throw new my_cool_error ()" deve ser tão lento.

Boa pergunta e estou ansioso para mais informações sobre este tema!


Eu mudei a resposta do @Mecki acima para ter o método1 retornando um booleano e uma checagem no método de chamada, já que você não pode simplesmente substituir uma Exceção por nada. Depois de duas execuções, o método 1 ainda era o mais rápido ou tão rápido quanto o método2.

Aqui está o instantâneo do código:

// Calculates without exception
public boolean method1(int i) {
    value = ((value + i) / i) << 1;
    // Will never be true
    return ((i & 0xFFFFFFF) == 1000000000);

}
....
   for (i = 1; i < 100000000; i++) {
            if (t.method1(i)) {
                System.out.println("Will never be true!");
            }
    }

e resultados:

Executar 1

method1 took 841 ms, result was 2
method2 took 841 ms, result was 2
method3 took 85058 ms, result was 2

Executar 2

method1 took 821 ms, result was 2
method2 took 838 ms, result was 2
method3 took 85929 ms, result was 2

O HotSpot é capaz de remover o código de exceção para exceções geradas pelo sistema, desde que esteja todo embutido. No entanto, exceções criadas explicitamente e aquelas que não foram removidas passam muito tempo criando o rastreamento de pilha. Substituir fillInStackTracepara ver como isso pode afetar o desempenho.





exception-handling