Qual é a sobrecarga real de try / catch em c #?

Então, eu sei que try / catch adiciona alguma sobrecarga e, portanto, não é uma boa maneira de controlar o stream do processo, mas de onde vem essa sobrecarga e qual é o impacto real dela?

Eu não sou um especialista em implementações de linguagem (por isso, leve isso em conta), mas acho que um dos maiores custos é desenrolar a pilha e armazená-la para o rastreamento de pilha. Eu suspeito que isso acontece apenas quando a exceção é lançada (mas eu não sei), e se assim for, isso seria decentemente custo escondido cada vez que uma exceção é lançada … por isso não é como você está apenas pulando de um lugar no código para outro, há muita coisa acontecendo.

Não acho que seja um problema, desde que você esteja usando exceções para o comportamento EXCEPCIONAL (portanto, não o caminho típico e esperado do programa).

Três pontos para fazer aqui:

  • Em primeiro lugar, há pouca ou nenhuma penalidade de desempenho em ter blocos try-catch em seu código. Isso não deve ser considerado ao tentar evitá-los em seu aplicativo. O hit de desempenho só entra em jogo quando uma exceção é lançada.

  • Quando uma exceção é lançada, além das operações de desenrolamento da pilha, etc. que acontecem, que outras mencionaram, você deve estar ciente de que um monte de coisas relacionadas ao tempo de execução / reflexo acontece para preencher os membros da class de exceção, como o rastreamento de pilha. object e os vários membros do tipo etc.

  • Eu acredito que esta é uma das razões pelas quais o conselho geral, se você está indo para relançar a exceção, é apenas throw; em vez de lançar a exceção novamente ou construir uma nova, como nesses casos, todas as informações da pilha são reunidas, enquanto no lançamento simples, todas são preservadas.

Você está perguntando sobre a sobrecarga de usar try / catch / finally quando exceções não são lançadas ou a sobrecarga de usar exceções para controlar o stream do processo? O último é um pouco parecido com o uso de um dinamite para acender a vela de aniversário de uma criança, e a sobrecarga associada cai nas seguintes áreas:

  • Você pode esperar falhas adicionais de cache devido à exceção gerada ao acessar dados residentes que normalmente não estão no cache.
  • Você pode esperar falhas de página adicionais devido à exceção gerada ao acessar código não residente e dados que não estão normalmente no conjunto de trabalho do seu aplicativo.

    • por exemplo, lançar a exceção exigirá que o CLR localize a localização dos blocos finally e catch com base no IP atual e no IP de retorno de cada quadro até que a exceção seja tratada mais o bloco de filtro.
    • custo adicional de construção e resolução de nomes para criar os frameworks para fins de diagnóstico, incluindo leitura de metadados etc.
    • ambos os itens acima normalmente acessam código e dados “frios”, portanto, falhas de página difíceis são prováveis ​​se você tiver pressão de memory:

      • o CLR tenta colocar código e dados que são usados ​​com pouca frequência em dados que são usados ​​com freqüência para melhorar a localidade, portanto isso funciona contra você porque você está forçando o frio a ficar quente.
      • o custo das falhas de página difícil, se houver, diminuirá todo o resto.
  • Situações típicas de captura geralmente são profundas, portanto, os efeitos acima tenderiam a ser ampliados (aumentando a probabilidade de falhas de página).

Quanto ao impacto real do custo, isso pode variar muito dependendo do que está acontecendo no código no momento. Jon Skeet tem um bom resumo aqui , com alguns links úteis. Eu tenho a tendência de concordar com sua declaração de que, se você chegar ao ponto em que as exceções estão prejudicando significativamente seu desempenho, você terá problemas em termos do uso de exceções além do desempenho.

Na minha experiência, a maior sobrecarga está na hora de lançar uma exceção e lidar com ela. Certa vez, trabalhei em um projeto em que um código semelhante ao seguinte era usado para verificar se alguém tinha o direito de editar algum object. Esse método HasRight () era usado em todos os lugares da camada de apresentação e era frequentemente chamado de centenas de objects.

 bool HasRight(string rightName, DomainObject obj) { try { CheckRight(rightName, obj); return true; } catch (Exception ex) { return false; } } void CheckRight(string rightName, DomainObject obj) { if (!_user.Rights.Contains(rightName)) throw new Exception(); } 

Quando o database de teste ficou mais cheio com dados de teste, isso levou a uma lentidão muito visível ao abrir novos formulários, etc.

Então eu refatorei isso para o seguinte, que – de acordo com as medições sujas rápidas mais recentes – é aproximadamente 2 ordens de magnitude mais rápida:

 bool HasRight(string rightName, DomainObject obj) { return _user.Rights.Contains(rightName); } void CheckRight(string rightName, DomainObject obj) { if (!HasRight(rightName, obj)) throw new Exception(); } 

Portanto, em resumo, o uso de exceções no stream normal do processo é cerca de duas ordens de grandeza mais lenta, usando stream de processo similar sem exceções.

Sem mencionar se está dentro de um método freqüentemente chamado, isso pode afetar o comportamento geral do aplicativo.
Por exemplo, considero o uso de Int32.Parse como uma prática ruim na maioria dos casos, uma vez que ele lança exceções para algo que pode ser capturado facilmente de outra forma.

Então, para concluir tudo escrito aqui:
1) Use os blocos try..catch para detectar erros inesperados – quase nenhuma penalidade de desempenho.
2) Não use exceções para erros de exceção se puder evitá-lo.

Eu escrevi um artigo sobre isso há algum tempo porque havia muita gente perguntando sobre isso na época. Você pode encontrá-lo e o código de teste em http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .

O resultado é que há uma pequena quantidade de sobrecarga para um bloco try / catch, mas tão pequeno que deve ser ignorado. No entanto, se você estiver executando blocos try / catch em loops executados milhões de vezes, talvez seja conveniente considerar mover o bloco para fora do loop, se possível.

O principal problema de desempenho com os blocos try / catch é quando você realmente captura uma exceção. Isso pode adicionar um atraso perceptível ao seu aplicativo. Claro, quando as coisas estão dando errado, a maioria dos desenvolvedores (e muitos usuários) reconhecem a pausa como uma exceção que está prestes a acontecer! A chave aqui é não usar exception handling para operações normais. Como o nome sugere, eles são excepcionais e você deve fazer tudo o que puder para evitar que sejam jogados. Você não deve usá-los como parte do stream esperado de um programa que está funcionando corretamente.

Eu fiz uma input de blog sobre esse assunto no ano passado. Confira. A conclusão é que quase não há custo para um bloco try se nenhuma exceção ocorrer – e no meu laptop, uma exceção era de aproximadamente 36μs. Isso pode ser menor do que o esperado, mas tenha em mente que esses resultados estão em uma pilha superficial. Além disso, as primeiras exceções são realmente lentas.

É muito mais fácil escrever, depurar e manter código livre de mensagens de erro do compilador, mensagens de aviso de análise de código e exceções de rotina aceitas (particularmente exceções que são lançadas em um local e aceitas em outro). Por ser mais fácil, o código será, em média, melhor escrito e com menos bugs.

Para mim, esse programador e sobrecarga de qualidade é o principal argumento contra o uso do try-catch para o stream do processo.

A sobrecarga de exceções do computador é insignificante em comparação e geralmente pequena em termos da capacidade do aplicativo de atender aos requisitos de desempenho do mundo real.

Eu realmente gosto do post do blog de Hafthor, e para adicionar meus dois centavos a esta discussão, eu gostaria de dizer que, sempre foi fácil para mim ter o DATA LAYER jogando apenas um tipo de exceção (DataAccessException). Desta forma, minha camada de negócios sabe que exceção esperar e pega. Em seguida, dependendo de outras regras de negócios (ou seja, se meu object de negócios participar do stream de trabalho, etc.), posso lançar uma nova exceção (BusinessObjectException) ou prosseguir sem re / throwing.

Eu diria que não hesite em usar o try..catch sempre que for necessário e usá-lo com sabedoria!

Por exemplo, esse método participa de um stream de trabalho …

Comentários?

 public bool DeleteGallery(int id) { try { using (var transaction = new DbTransactionManager()) { try { transaction.BeginTransaction(); _galleryRepository.DeleteGallery(id, transaction); _galleryRepository.DeletePictures(id, transaction); FileManager.DeleteAll(id); transaction.Commit(); } catch (DataAccessException ex) { Logger.Log(ex); transaction.Rollback(); throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex); } } } catch (DbTransactionException ex) { Logger.Log(ex); throw new BusinessObjectException("Cannot delete gallery.", ex); } return true; } 

Podemos ler em Programming Language Pragmatics, de Michael L. Scott, que os compiladores atuais não adicionam nenhuma sobrecarga no caso comum, ou seja, quando nenhuma exceção ocorre. Então todo trabalho é feito em tempo de compilation. Mas quando uma exceção é lançada em tempo de execução, o compilador precisa executar uma pesquisa binária para encontrar a exceção correta e isso acontecerá para cada novo lance que você fez.

Mas as exceções são exceções e esse custo é perfeitamente aceitável. Se você tentar fazer Exception Handling sem exceções e usar códigos de erro de retorno, provavelmente precisará de uma instrução if para cada sub-rotina e isso resultará em uma sobrecarga em tempo real. Você sabe que uma instrução if é convertida em algumas instruções de assembly, que serão executadas toda vez que você inserir suas sub-rotinas.

Desculpe pelo meu inglês, espero que isso ajude você. Esta informação é baseada no livro citado, para mais informações, consulte o Capítulo 8.5 Tratamento de Exceção.

Ao contrário das teorias comumente aceitas, try / catch pode ter implicações significativas no desempenho, e é se uma exceção é lançada ou não!

  1. Desativa algumas otimizações automáticas (por design) e, em alguns casos, injeta o código de debugging , como você pode esperar de um auxílio de debugging . Sempre haverá pessoas que discordam de mim nesse ponto, mas a linguagem exige isso e a desassembly mostra isso para que essas pessoas fiquem delirando por definição de dictionary.
  2. Pode afetar negativamente a manutenção. Esta é realmente a questão mais importante aqui, mas desde que a minha última resposta (que se concentrou quase inteiramente) foi eliminada, vou tentar focar a questão menos significativa (a micro-otimização) em oposição à questão mais significativa ( a otimização macro).

O primeiro foi abordado em alguns posts de blog por MVPs da Microsoft ao longo dos anos, e eu acredito que você poderia encontrá-los facilmente, mas o StackOverflow se preocupa tanto com conteúdo, então vou fornecer links para alguns deles como evidência de preenchimento :

  • Implicações de desempenho de try / catch / finally ( e parte dois ), por Peter Ritchie, explora as otimizações que try / catch / finally desativa (e eu irei além disso com citações do padrão)
  • Análise de desempenho Parse vs TryParse vs ConvertTo por Ian Huff afirma descaradamente que “exception handling é muito lenta” e demonstra esse ponto colocando Int.Parse e Int.TryParse contra um ao outro … Para qualquer um que insista que TryParse usa try / catch nos bastidores, isso deve lançar alguma luz!

Há também esta resposta que mostra a diferença entre o código desmontado com e sem o uso de try / catch .

Parece tão óbvio que existe uma sobrecarga que é flagrantemente observável na geração de código, e essa sobrecarga parece mesmo ser reconhecida por pessoas que a Microsoft valoriza! Ainda estou repetindo a internet

Sim, existem dezenas de instruções MSIL extras para uma linha de código trivial, e isso nem cobre as otimizações desativadas, portanto, tecnicamente, é uma otimização micro.


Eu postei uma resposta anos atrás, que foi excluída, pois se concentrava na produtividade dos programadores (a otimização macro).

Isso é lamentável, já que não há economia de alguns nanossegundos aqui e ali do tempo de CPU, que provavelmente compensará muitas horas acumuladas de otimização manual por humanos. Qual seu chefe paga mais por: uma hora do seu tempo ou uma hora com o computador funcionando? Em que ponto nós desligamos e admitimos que é hora de comprar um computador mais rápido ?

Obviamente, devemos otimizar nossas prioridades , não apenas nosso código! Na minha última resposta, baseei-me nas diferenças entre dois trechos de código.

Usando try / catch :

 int x; try { x = int.Parse("1234"); } catch { return; } // some more code here... 

Não usando try / catch :

 int x; if (int.TryParse("1234", out x) == false) { return; } // some more code here 

Considere a partir da perspectiva de um desenvolvedor de manutenção, que é mais provável desperdiçar seu tempo, se não em criação de perfil / otimização (coberto acima), que provavelmente nem seria necessário se não fosse pelo problema try / catch , então em rolando pelo código-fonte … Um desses tem quatro linhas extras de lixo clichê!

À medida que mais e mais campos são introduzidos em uma class, todo esse lixo clichê se acumula (tanto na fonte quanto no código desmontado) bem além dos níveis razoáveis. Quatro linhas extras por campo, e elas são sempre as mesmas linhas … Não fomos ensinados a evitar a repetição de nós mesmos? Eu suponho que nós poderíamos esconder o try / catch atrás de alguma abstração feita em casa, mas … então nós poderíamos evitar exceções (isto é, usar Int.TryParse ).

Isso nem é um exemplo complexo; Eu vi tentativas de instanciar novas classs em try / catch . Considere que todo o código dentro do construtor pode ser desqualificado de certas otimizações que seriam aplicadas automaticamente pelo compilador. Que melhor maneira de dar origem à teoria de que o compilador é lento , ao contrário do compilador está fazendo exatamente o que é dito para fazer ?

Supondo que uma exceção seja lançada pelo dito construtor, e algum bug é acionado como resultado, o desenvolvedor de manutenção deficiente precisa rastreá-lo. Isso pode não ser uma tarefa tão fácil, já que ao contrário do código espaguete do pesadelo goto , try / catch pode causar bagunça em três dimensões , pois ele pode subir a pilha não apenas em outras partes do mesmo método, mas também em outras classs. e methods, todos os quais serão observados pelo desenvolvedor de manutenção, da maneira mais difícil ! Ainda nos dizem que “goto é perigoso”, heh!

No final eu menciono, try / catch tem seu benefício, ou seja, ele é projetado para desabilitar otimizações ! É, se você quiser, uma ajuda de debugging ! Foi para isso que foi projetado e é o que deve ser usado como …

Eu acho que é um ponto positivo também. Ele pode ser usado para desabilitar otimizações que, de outra forma, poderiam comprometer algoritmos de passagem de mensagens sãs para aplicativos multithread e capturar possíveis condições de corrida;) Esse é o único cenário em que consigo pensar em usar try / catch. Até isso tem alternativas.


Quais otimizações try , catch e finally desabilitam?

AKA

Como são try , catch e finally úteis como auxiliares de debugging?

eles são barreiras de escrita. Isso vem do padrão:

12.3.3.13 Declarações de try-catch

Para uma declaração do formulário:

 try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n 
  • O estado de atribuição definido de v no início de try-block é o mesmo que o estado de atribuição definido de v no início de stmt .
  • O estado de atribuição definido de v no início de catch-block-i (para qualquer i ) é o mesmo que o estado de atribuição definido de v no início de stmt .
  • O estado de atribuição definido de v no ponto final de stmt é definitivamente atribuído se (e somente se) v for definitivamente atribuído no ponto final de try-block e cada catch-block-i (para cada i de 1 an ).

Em outras palavras, no início de cada instrução try :

  • todas as atribuições feitas a objects visíveis antes de entrar na instrução try devem ser completas, o que requer um bloqueio de thread para uma partida, tornando-a útil para depurar condições de corrida!
  • o compilador não tem permissão para:
    • eliminar atribuições de variables ​​não utilizadas que foram definitivamente atribuídas a antes da instrução try
    • reorganizar ou unir qualquer uma de suas atribuições internas (ou seja, ver meu primeiro link, se você ainda não tiver feito isso).
    • gere atribuições sobre esta barreira, para atrasar a atribuição a uma variável que saiba que não será usada até mais tarde (se for o caso) ou para antecipadamente mover atribuições posteriores adiante para fazer outras otimizações possíveis …

Uma história semelhante vale para cada declaração catch ; suponha que dentro de sua instrução try (ou um construtor ou function que invoca, etc) você atribui àquela variável sem sentido (vamos dizer, garbage=42; ), o compilador não pode eliminar essa afirmação, não importa quão irrelevante seja para o comportamento observável do programa. A atribuição precisa ser concluída antes que o bloco catch seja inserido.

Por que vale a pena, finally conta uma história similarmente degradante :

12.3.3.14 Declarações try-finally

Para um comando try statement do formulário:

 try try-block finally finally-block 

• O estado de atribuição definido de v no início de try-block é o mesmo que o estado de atribuição definido de v no início de stmt .
• O estado de atribuição definido de v no início de finally-block é o mesmo que o estado de atribuição definido de v no início de stmt .
• O estado de atribuição definido de v no ponto final de stmt é definitivamente atribuído se (e somente se) ou: o v é definitivamente atribuído no ponto final de try-block o v é definitivamente atribuído no ponto final de finally-block Se uma transferência de stream de controle (como uma instrução goto ) é feita que começa dentro de try-block e termina fora de try-block , então v também é considerado definitivamente atribuído àquela transferência de stream de controle se v é definitivamente atribuído em o ponto final do bloco final . (Este não é um único caso – se v é definitivamente atribuído por outro motivo nesta transferência de stream de controle, então ele ainda é considerado definitivamente atribuído.)

12.3.3.15 Declarações try-catch-finally

Análise de atribuição definitiva para uma declaração trycatchfinally do formulário:

 try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n finally finally-block 

é feito como se a declaração fosse uma declaração tryfinally anexando uma instrução trycatch :

 try { try try-block catch ( ... ) catch-block-1 ... catch ( ... ) catch-block-n } finally finally-block 

Vamos analisar um dos maiores custos possíveis de um bloco try / catch quando usado onde não deveria precisar ser usado:

 int x; try { x = int.Parse("1234"); } catch { return; } // some more code here... 

E aqui está o único sem try / catch:

 int x; if (int.TryParse("1234", out x) == false) { return; } // some more code here 

Sem contar o insignificante espaço em branco, pode-se notar que esses dois pedaços iguais de código têm quase exatamente o mesmo comprimento em bytes. Este último contém 4 bytes a menos de indentação. Isso é uma coisa ruim?

Para adicionar insulto à injúria, o aluno decide dar um loop enquanto a input pode ser analisada como um int. A solução sem try / catch pode ser algo como:

 while (int.TryParse(...)) { ... } 

Mas como isso se parece ao usar try / catch?

 try { for (;;) { x = int.Parse(...); ... } } catch { ... } 

Blocos de tentativa / captura são maneiras mágicas de desperdiçar recuo, e nós ainda nem sabemos o motivo pelo qual ele falhou! Imagine como a pessoa que está depurando se sente, quando o código continua a executar além de uma falha lógica séria, em vez de parar com um erro de exceção óbvio. Blocos de tentativa / captura são validação / saneamento de dados de um homem preguiçoso.

Um dos menores custos é que os blocos try / catch realmente desabilitam certas otimizações: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Eu acho que é um ponto positivo também. Ele pode ser usado para desabilitar otimizações que, de outra forma, poderiam comprometer algoritmos de passagem de mensagens sãs para aplicativos multithread e capturar possíveis condições de corrida;) Esse é o único cenário em que consigo pensar em usar try / catch. Até isso tem alternativas.