As exceções em C ++ são muito lentas

98

Eu estava assistindo Systematic Error Handling in C ++ - Andrei Alexandrescu afirma que as exceções em C ++ são muito lentas.

Isso ainda é verdade para C ++ 98?

Avinash
fonte
42
Não faz sentido perguntar se as "exceções C ++ 98" são mais rápidas / mais lentas do que as "exceções C ++ 03" ou "exceções C ++ 11". Seu desempenho depende de como o compilador os implementa em seus programas, e o padrão C ++ não diz nada sobre como eles devem ser implementados; o único requisito é que seu comportamento siga o padrão (a regra "como se").
In silico
Pergunta relacionada (mas não realmente duplicada): stackoverflow.com/questions/691168/…
Philipp
2
sim, é muito lento, mas não deve ser jogado para uma operação normal ou usado como um ramo
BЈовић
Eu encontrei uma pergunta semelhante .
PaperBirdMaster
Para esclarecer o que BЈовић disse, usar exceções não é algo para se temer. É quando uma exceção é lançada que você encontra operações (potencialmente) demoradas. Também estou curioso para saber por que você deseja saber para C ++ 89 especificamente ... que a versão mais recente é C ++ 11, e o tempo que leva para as exceções serem executadas é a implementação definida, portanto, meu "potencialmente" demorado .
thecoshman

Respostas:

162

O principal modelo usado hoje para exceções (Itanium ABI, VC ++ 64 bits) são as exceções do modelo Zero-Cost.

A ideia é que em vez de perder tempo configurando um guarda e verificando explicitamente a presença de exceções em todos os lugares, o compilador gere uma tabela lateral que mapeia qualquer ponto que possa lançar uma exceção (Contador de programa) para uma lista de manipuladores. Quando uma exceção é lançada, esta lista é consultada para escolher o manipulador correto (se houver) e a pilha é desfeita.

Em comparação com a if (error)estratégia típica :

  • o modelo Zero-Cost, como o nome indica, é gratuito quando não ocorrem exceções
  • custa cerca de 10x / 20x e ifquando ocorre uma exceção

O custo, no entanto, não é trivial de medir:

  • A mesa lateral geralmente está fria e, portanto, recuperá-la da memória leva muito tempo
  • Determinar o manipulador certo envolve RTTI: muitos descritores RTTI para buscar, espalhados pela memória e operações complexas para executar (basicamente um dynamic_castteste para cada manipulador)

Portanto, a maioria das falhas de cache e, portanto, não é trivial em comparação com o código puro da CPU.

Nota: para mais detalhes, leia o relatório TR18015, capítulo 5.4 Tratamento de exceções (pdf)

Portanto, sim, as exceções são lentas no caminho excepcional , mas são mais rápidas do que as verificações explícitas ( ifestratégia) em geral.

Nota: Andrei Alexandrescu parece questionar isso "mais rápido". Eu pessoalmente vi as coisas mudarem para os dois lados, alguns programas sendo mais rápidos com exceções e outros sendo mais rápidos com ramificações, então, de fato, parece haver uma perda de otimizabilidade em certas condições.


Isso importa ?

Eu diria que não. Um programa deve ser escrito tendo em mente a legibilidade , não o desempenho (pelo menos, não como primeiro critério). Exceções devem ser usadas quando se espera que o chamador não possa ou não deseje lidar com a falha na hora e passá-la para cima na pilha. Bônus: em C ++ 11, as exceções podem ser distribuídas entre threads usando a Biblioteca Padrão.

Isso é sutil, porém, eu afirmo que map::findnão deveria jogar, mas estou bem em map::findretornar um checked_ptrque joga se uma tentativa de cancelar a referência falhar porque é nulo: no último caso, como no caso da classe que Alexandrescu introduziu, o chamador escolhe entre a verificação explícita e a dependência de exceções. Capacitar o chamador sem dar a ele mais responsabilidade geralmente é um sinal de bom design.

Matthieu M.
fonte
3
+1 Eu acrescentaria apenas quatro coisas: (0) sobre o suporte para relançar adicionado em C ++ 11; (1) uma referência ao relatório do comitê sobre a eficiência de c ++; (2) algumas observações sobre correção (como superar até mesmo legibilidade); e (3) sobre o desempenho, observações sobre medi-lo em relação ao caso de não usar exceções (tudo é relativo)
Felicidades e hth. - Alf
2
@ Cheersandhth.-Alf: (0), (1) e (3) feito: obrigado. Com relação à correção (2), embora supere a legibilidade, não tenho certeza sobre as exceções que levam a um código mais correto do que outras estratégias de tratamento de erros (é tão fácil esquecer os muitos caminhos invisíveis de criação de exceções de execução).
Matthieu M.
2
A descrição pode ser localmente correta, mas pode ser interessante notar que a presença de exceções tem implicações globais nas suposições e otimizações que o compilador pode fazer. Essas implicações sofrem o problema de "não terem contra-exemplos triviais", já que o compilador sempre pode ver através de um pequeno programa. A criação de perfil em uma base de código grande e realista com e sem exceções pode ser uma boa ideia.
Kerrek SB
4
> o modelo Zero-Cost, como o nome indica, é gratuito quando não ocorre nenhuma exceção, isso não é realmente verdadeiro nos melhores níveis de detalhe. gerar mais código sempre tem um impacto no desempenho, mesmo que pequeno e sutil ... pode demorar um pouco mais para o sistema operacional carregar o executável, ou você terá mais erros de i-cache. além disso, que tal código de desenrolamento de pilha? além disso, e quanto aos experimentos que você pode fazer para medir os efeitos em vez de tentar entendê-los com o pensamento racional?
jheriko
2
@jheriko: Na verdade, acredito que já respondi à maioria de suas perguntas. O tempo de carregamento não deve ser afetado (o código frio não deve ser carregado), o i-cache não deve ser afetado (o código frio não deve entrar no i-cache), ... para resolver a questão que falta: "how to measure" => substituir qualquer exceção lançada por uma chamada para abortpermitirá que você meça a pegada de tamanho binário e verifique se o tempo de carregamento / i-cache se comporta de maneira semelhante. Claro, melhor não acertar em nenhum dos abort...
Matthieu M.
60

Quando a pergunta foi postada eu estava a caminho do médico, com um táxi esperando, então só tive tempo para um breve comentário. Mas tendo agora comentado e votado a favor e contra, é melhor adicionar minha própria resposta. Mesmo que a resposta de Matthieu já seja muito boa.


As exceções são especialmente lentas em C ++, em comparação com outras linguagens?

Re a reivindicação

“Eu estava assistindo Systematic Error Handling in C ++ - Andrei Alexandrescu, ele afirma que as exceções em C ++ são muito lentas.”

Se isso é literalmente o que Andrei afirma, então pela primeira vez ele é muito enganador, se não totalmente errado. Para exceções levantadas / lançadas é sempre lento em comparação com outras operações básicas na linguagem, independentemente da linguagem de programação . Não apenas em C ++ ou mais em C ++ do que em outras linguagens, como indica a alegação.

Em geral, principalmente independentemente do idioma, os dois recursos básicos da linguagem que são ordens de magnitude mais lentos do que o resto, porque se traduzem em chamadas de rotinas que lidam com estruturas de dados complexas, são

  • lançamento de exceção, e

  • alocação de memória dinâmica.

Felizmente, em C ++, muitas vezes é possível evitar ambos no código de tempo crítico.

Infelizmente, não existe almoço grátis , mesmo que a eficiência padrão do C ++ chegue bem perto. :-) Pois a eficiência ganha ao evitar o lançamento de exceções e a alocação dinâmica de memória geralmente é obtida pela codificação em um nível inferior de abstração, usando C ++ apenas como um “C melhor”. E menor abstração significa maior “complexidade”.

Maior complexidade significa mais tempo gasto em manutenção e pouco ou nenhum benefício com a reutilização de código, que são custos monetários reais, mesmo se difíceis de estimar ou medir. Ou seja, com C ++ pode-se, se desejar, trocar alguma eficiência do programador por eficiência de execução. Fazer isso é em grande parte uma decisão de engenharia e intuição, porque na prática apenas o ganho, e não o custo, pode ser facilmente estimado e medido.


Há alguma medida objetiva de desempenho de lançamento de exceção C ++?

Sim, o comitê internacional de padronização C ++ publicou um relatório técnico sobre o desempenho do C ++, TR18015 .


O que significa que as exceções são “lentas”?

Significa principalmente que um throwpode demorar muito tempo ™ em comparação a, por exemplo, uma intatribuição, devido à procura do manipulador.

Como TR18015 discute em sua seção 5.4 "Exceções", existem duas estratégias principais de implementação de tratamento de exceção,

  • a abordagem em que cada try-bloco configura dinamicamente a captura de exceções, de modo que uma pesquisa na cadeia dinâmica de manipuladores seja realizada quando uma exceção é lançada, e

  • a abordagem em que o compilador gera tabelas de consulta estáticas que são usadas para determinar o manipulador de uma exceção lançada.

A primeira abordagem muito flexível e geral é quase forçada no Windows de 32 bits, enquanto na terra de 64 bits e em * nix-land a segunda abordagem muito mais eficiente é comumente usada.

Além disso, conforme o relatório discute, para cada abordagem, existem três áreas principais onde o tratamento de exceções impacta a eficiência:

  • try-blocos,

  • funções regulares (oportunidades de otimização), e

  • throw-expressões.

Principalmente, com a abordagem do manipulador dinâmico (Windows de 32 bits), o tratamento de exceções tem um impacto nos tryblocos, principalmente independentemente da linguagem (porque isso é forçado pelo esquema de Tratamento de Exceções Estruturado do Windows ), enquanto a abordagem de tabela estática tem custo praticamente zero para try- blocos. Discutir isso exigiria muito mais espaço e pesquisa do que é prático para uma resposta SO. Portanto, consulte o relatório para obter detalhes.

Infelizmente o relatório, de 2006, já está um pouco datado do final de 2012 e, pelo que eu sei, não há nada comparável que seja mais recente.

Outra perspectiva importante é que o impacto do uso de exceções no desempenho é muito diferente da eficiência isolada dos recursos de linguagem de suporte, porque, como observa o relatório,

“Ao considerar o tratamento de exceções, deve-se comparar as formas alternativas de lidar com os erros.”

Por exemplo:

  • Custos de manutenção devido a diferentes estilos de programação (correção)

  • ifVerificação de falha de site de chamada redundante versus centralizadatry

  • Problemas de cache (por exemplo, um código mais curto pode caber no cache)

O relatório tem uma lista diferente de aspectos a serem considerados, mas de qualquer forma a única maneira prática de obter fatos concretos sobre a eficiência de execução é provavelmente implementar o mesmo programa usando exceção e não usando exceções, dentro de um limite definido no tempo de desenvolvimento e com desenvolvedores familiarizado com cada forma e, em seguida, MEASURE .


Qual é uma boa maneira de evitar a sobrecarga de exceções?

A correção quase sempre supera a eficiência.

Sem exceções, o seguinte pode acontecer facilmente:

  1. Algum código P destina-se a obter um recurso ou computar algumas informações.

  2. O código de chamada C deveria ter verificado o sucesso / falha, mas não o faz.

  3. Um recurso inexistente ou informação inválida é usado no código após C, causando confusão geral.

O principal problema é o ponto (2), onde com o esquema usual de código de retorno , o código de chamada C não é forçado a verificar.

Existem duas abordagens principais que forçam essa verificação:

  • Onde P lança uma exceção diretamente quando falha.

  • Onde P retorna um objeto que C deve inspecionar antes de usar seu valor principal (caso contrário, uma exceção ou terminação).

A segunda abordagem foi, AFAIK, descrita pela primeira vez por Barton e Nackman em seu livro * Scientific and Engineering C ++: Uma introdução com técnicas avançadas e exemplos , onde eles introduziram uma classe chamada Fallowpara um resultado de função “possível”. Uma classe semelhante chamada optionalagora é oferecida pela biblioteca Boost. E você mesmo pode implementar facilmente uma Optionalclasse, usando um std::vectorportador de valor para o caso de resultado não POD.

Com a primeira abordagem, o código de chamada C não tem escolha a não ser usar técnicas de tratamento de exceções. Com a segunda abordagem, no entanto, o código de chamada C pode decidir se fará a ifverificação baseada ou o tratamento geral de exceção. Assim, a segunda abordagem permite fazer o compromisso entre o programador e a eficiência do tempo de execução.


Qual é o impacto dos vários padrões C ++ no desempenho de exceção?

“Eu quero saber se isso ainda é verdade para C ++ 98”

C ++ 98 foi o primeiro padrão C ++. Para exceções, ele introduziu uma hierarquia padrão de classes de exceção (infelizmente, um tanto imperfeita). O principal impacto no desempenho foi a possibilidade de especificações de exceção (removidas no C ++ 11), que, entretanto, nunca foram totalmente implementadas pelo compilador principal do Windows C ++ Visual C ++: Visual C ++ aceita a sintaxe de especificação de exceção do C ++ 98, mas apenas ignora especificações de exceção.

C ++ 03 foi apenas uma retificação técnica de C ++ 98. A única realmente nova no C ++ 03 era a inicialização de valor . O que não tem nada a ver com exceções.

Com o C ++ 11, as especificações gerais de exceção padrão foram removidas e substituídas pela noexceptpalavra - chave.

O padrão C ++ 11 também adicionou suporte para armazenar e relançar exceções, o que é ótimo para propagar exceções C ++ em retornos de chamada da linguagem C. Esse suporte restringe efetivamente como a exceção atual pode ser armazenada. No entanto, até onde eu sei, isso não afeta o desempenho, exceto na medida em que no código mais recente, o tratamento de exceções pode ser usado mais facilmente em ambos os lados de um retorno de chamada da linguagem C.

Saúde e hth. - Alf
fonte
6
"exceções é sempre lento em comparação com outras operações básicas na linguagem, independentemente da linguagem de programação" ... exceto em linguagens projetadas para compilar o uso de exceções no controle de fluxo normal.
Ben Voigt
4
"lançar uma exceção envolve tanto a alocação quanto o empilhamento". Isso também obviamente não é verdade em geral e, novamente, OCaml é um contra-exemplo. Em linguagens com coleta de lixo, não há necessidade de desenrolar a pilha porque não há destruidores, portanto, você apenas longjmpacessa o manipulador.
JD
2
@JonHarrop: provavelmente você não sabe que o Pyhon tem uma cláusula final para tratamento de exceções. isso significa que uma implementação Python tem desdobramento de pilha ou não é Python. você parece ignorar completamente os assuntos sobre os quais faz afirmações (de fantasia). Desculpe.
Saúde e hth. - Alf
2
@ Cheersandhth.-Alf: "Pyhon tem uma cláusula finally para tratamento de exceções. Isso significa que uma implementação Python tem pilha desenrolada ou não é Python". A try..finallyconstrução pode ser implementada sem desenrolar da pilha. F #, C # e Java são implementados try..finallysem usar o desenrolamento de pilha. Você apenas longjmppara o handler (como já expliquei).
JD
4
@JonHarrop: parece que você está colocando um dilema. mas não tem nenhuma relevância que eu possa ver para qualquer coisa discutida até agora, e até agora você postou uma longa sequência de bobagens que soam negativas . eu teria que confiar em você para concordar ou não com algumas palavras vagas, porque como antagonista você está escolhendo o que irá revelar que "significa", e eu certamente não confio em você depois de todas aquelas bobagens sem sentido, votos negativos etc.
Saúde e hth. - Alf
13

Você nunca pode reclamar sobre o desempenho, a menos que converta o código para o assembly ou faça um benchmark.

Aqui está o que você vê: (banco rápido)

O código de erro não é sensível à porcentagem de ocorrência. As exceções têm um pouco de sobrecarga, desde que nunca sejam lançadas. Depois de jogá-los, a miséria começa. Neste exemplo, ele é lançado para 0%, 1%, 10%, 50% e 90% dos casos. Quando as exceções são lançadas 90% do tempo, o código é 8 vezes mais lento do que o caso em que as exceções são lançadas 10% do tempo. Como você vê, as exceções são muito lentas. Não os use se forem lançados com frequência. Se seu aplicativo não tiver requisitos de tempo real, sinta-se à vontade para lançá-los se ocorrerem muito raramente.

Você vê muitas opiniões contraditórias sobre eles. Mas, finalmente, as exceções são lentas? Eu não julgo. Basta observar o benchmark.

Comparativo de desempenho de exceções C ++

Arash
fonte
12

Depende do compilador.

O GCC, por exemplo, era conhecido por ter um desempenho muito ruim ao lidar com exceções, mas isso melhorou consideravelmente nos últimos anos.

Mas observe que o tratamento de exceções deve - como o nome diz - ser a exceção e não a regra no design de seu software. Quando você tem um aplicativo que lança tantas exceções por segundo que afeta o desempenho e isso ainda é considerado uma operação normal, você deve pensar em fazer as coisas de forma diferente.

As exceções são uma ótima maneira de tornar o código mais legível, tirando do caminho todo aquele código desajeitado de tratamento de erros, mas assim que se tornam parte do fluxo normal do programa, elas se tornam realmente difíceis de seguir. Lembre-se de que a throwé basicamente um goto catchdisfarce.

Philipp
fonte
-1 re a questão como está agora, "isso ainda é verdade para C ++ 98", que certamente não depende do compilador. também, esta resposta throw new Exceptioné um Java-ismo. como regra, nunca se deve lançar ponteiros.
Saúde e hth. - Alf
1
o padrão 98 dita exatamente como as exceções devem ser implementadas?
thecoshman
6
C ++ 98 é um padrão ISO, não um compilador. Existem muitos compiladores que o implementam.
Philipp
3
@thecoshman: Não. O padrão C ++ não diz nada sobre como qualquer coisa deve ser implementada (com a possível exceção da parte "Limites de implementação" do padrão).
In silico
2
@Insilico, então, só posso tirar a conclusão lógica de que (chocantemente) é a implementação definida (leitura, compilador específico) como as exceções funcionam.
thecoshman
4

Sim, mas isso não importa. Por quê?
Leia:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

Basicamente, isso diz que usar exceções como Alexandrescu descreveu (desaceleração 50x porque eles usam catchcomo else) é simplesmente errado. Dito isso, para pessoas que gostam de fazer assim, eu desejo que C ++ 22 :) adicionaria algo como:
(note que isso teria que ser a linguagem principal, pois é basicamente um compilador gerando código a partir de um existente)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS também note que mesmo que as exceções sejam tão lentas ... não é um problema se você não gastar muito tempo nessa parte do código durante a execução ... Por exemplo, se a divisão flutuante for lenta e você a aumentar 4x mais rápido, não importa se você gasta 0,3% do seu tempo fazendo a divisão de FP ...

NoSenseEtAl
fonte
0

Como in silico disse que sua implementação depende, mas em geral as exceções são consideradas lentas para qualquer implementação e não devem ser usadas em código de alto desempenho.

EDIT: Não estou dizendo para não usá-los, mas para código intensivo de desempenho, é melhor evitá-los.

Chris McCabe
fonte
9
Essa é, na melhor das hipóteses, uma maneira muito simplista de observar o desempenho da exceção. Por exemplo, o GCC usa uma implementação de "custo zero" onde você não incorre em um impacto de desempenho se nenhuma exceção for lançada. E as exceções são destinadas a circunstâncias excepcionais (ou seja, raras), então, mesmo que sejam lentas por alguma métrica, ainda não é razão suficiente para não usá-las.
In silico
@insilico se você olhar por que eu disse, eu não disse para não usar exceções e ponto final. Especifiquei um código de desempenho intensivo, esta é uma avaliação precisa, eu trabalho principalmente com gpgpus e seria morto se usasse exceções.
Chris McCabe