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

94

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

JC Grubbs
fonte

Respostas:

52

Não sou um especialista em implementações de linguagem (portanto, considere isso com cautela), mas acho que um dos maiores custos é desfazer a pilha e armazená-la para o rastreamento de pilha. Suspeito que isso aconteça apenas quando a exceção é lançada (mas eu não sei), e se sim, isso seria um custo oculto de tamanho decente toda vez que uma exceção é lançada ... então não é como se você estivesse 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 comportamento EXCEPCIONAL (portanto, não é o seu caminho típico e esperado através do programa).

Mike Stone
fonte
33
Mais precisamente: tentar é barato, pegar é barato, jogar é caro. Se você evitar tentar pegar, o lançamento ainda é caro.
Programador do Windows
4
Hmmm - a marcação não funciona nos comentários. Para tentar novamente - as exceções são para erros, não para "comportamento excepcional" ou condições: blogs.msdn.com/kcwalina/archive/2008/07/17/…
HTTP 410
2
@Windows programmer Stats / source please?
Kapé de
100

Três pontos a serem destacados aqui:

  • Em primeiro lugar, há pouca ou nenhuma penalidade de desempenho em realmente ter blocos try-catch em seu código. Isso não deve ser levado em consideração ao tentar evitar tê-los em seu aplicativo. O impacto no 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 ocorrem, as quais outros mencionaram, você deve estar ciente de que um monte de coisas relacionadas ao tempo de execução / reflexão acontecem para preencher os membros da classe de exceção, como o rastreamento de pilha objeto e os vários membros de tipo etc.

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

Shaun Austin
fonte
41
Além disso: ao relançar uma exceção como "lançar ex", você perde o rastreio de pilha original e o substitui pelo rastreio de pilha CURRENT; raramente o que é desejado. Se você apenas "lançar", o rastreamento da pilha original na Exceção será preservado.
Eddie
@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki
21

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

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

    • por exemplo, lançar a exceção exigirá que o CLR encontre 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 nome para criar os quadros para fins de diagnóstico, incluindo leitura de metadados etc.
    • ambos os itens acima normalmente acessam código e dados "frios", então as falhas de página são prováveis ​​se você tiver pressão de memória:

      • o CLR tenta colocar o código e os dados usados ​​raramente longe dos dados usados ​​com frequência para melhorar a localidade, então isso funciona contra você porque você está forçando o frio a esquentar.
      • o custo das falhas de página física, se houver, vai ofuscar todo o resto.
  • Situações típicas de captura são frequentemente profundas, portanto, os efeitos acima tendem a ser ampliados (aumentando a probabilidade de falhas de página).

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

HTTP 410
fonte
6

Na minha experiência, a maior sobrecarga é realmente lançar uma exceção e tratá-la. 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 objeto. Este método HasRight () era usado em todos os lugares na camada de apresentação e era freqüentemente chamado para centenas de objetos.

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 banco de dados de teste fica mais cheio de dados de teste, isso leva a uma desaceleração muito visível ao abrir novos formulários, etc.

Então, eu refatorei para o seguinte, que - de acordo com medições rápidas e sujas posteriores - é cerca de 2 ordens de magnitude mais rápido:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

Resumindo, usar exceções no fluxo de processo normal é cerca de duas ordens de magnitude mais lento do que usar um fluxo de processo semelhante sem exceções.

Tobi
fonte
1
Por que você deseja lançar uma exceção aqui? Você poderia lidar com o caso de não ter os direitos no local.
ThunderGr
@ThunderGr foi isso que eu mudei, tornando-o duas ordens de magnitude mais rápido.
Tobi,
5

Ao contrário das teorias comumente aceitas, try/ catchpode ter implicações significativas no desempenho, independentemente de uma exceção ser lançada ou não!

  1. Ele desativa algumas otimizações automáticas (por design) e, em alguns casos, injeta código de depuração , como você pode esperar de um auxílio de depuração . Sempre haverá pessoas que discordarão de mim neste ponto, mas a linguagem exige isso e a desmontagem mostra isso, de modo que essas pessoas estão, por definição de dicionário, delirando .
  2. Isso pode impactar negativamente na manutenção. Na verdade, este é o problema mais significativo aqui, mas como minha última resposta (que se concentrava quase inteiramente nele) foi excluída, tentarei me concentrar no problema menos significativo (a micro-otimização) em oposição ao problema mais significativo ( a macro-otimização).

O primeiro foi abordado em algumas postagens de blog de MVPs da Microsoft ao longo dos anos, e acredito que você possa encontrá-los facilmente, mas o StackOverflow se preocupa muito com o conteúdo, então vou fornecer links para alguns deles como prova de preenchimento :

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

Parece tão óbvio que não é uma sobrecarga que é descaradamente observável na geração de código, e que a sobrecarga parece mesmo ser reconhecido por pessoas que valor Microsoft! Ainda estou, repetindo a internet ...

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


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

Isso é lamentável, pois nenhuma economia de alguns nanossegundos aqui e ali no tempo da CPU provavelmente compensará muitas horas acumuladas de otimização manual por humanos. O que seu chefe paga mais: uma hora do seu tempo ou uma hora com o computador ligado? Em que ponto desligamos a tomada e admitimos que é hora de comprar um computador mais rápido ?

Obviamente, devemos otimizar nossas prioridades , não apenas nosso código! Em minha última resposta, usei as diferenças entre dois fragmentos 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 da perspectiva de um desenvolvedor de manutenção, que é mais provável que perca seu tempo, se não no perfil / otimização (coberto acima), o que provavelmente nem seria necessário se não fosse pelo problema try/ catch, então rolar através código-fonte ... Um deles tem quatro linhas extras de lixo clichê!

À medida que mais e mais campos são introduzidos em uma classe, todo esse lixo clichê se acumula (tanto no código-fonte quanto no código desmontado) muito além dos níveis razoáveis. Quatro linhas extras por campo, e são sempre as mesmas linhas ... Não fomos ensinados a evitar a repetição? Suponho que poderíamos esconder o try/ catchatrás de alguma abstração caseira, mas ... então podemos também evitar exceções (ou seja, usarInt.TryParse ).

Este não é nem mesmo um exemplo complexo; Tenho visto tentativas de instanciar novas classes em try/ catch. Considere que todo o código dentro do construtor pode então ser desqualificado de certas otimizações que seriam aplicadas automaticamente pelo compilador. Qual a melhor maneira de dar origem à teoria de que o compilador é lento , ao contrário de que o compilador está fazendo exatamente o que foi mandado ?

Supondo que uma exceção seja lançada pelo referido construtor, e algum bug seja acionado como resultado, o desenvolvedor de manutenção deficiente deve rastreá-lo. Isso pode não ser uma tarefa tão fácil, pois ao contrário do código espaguete do pesadelo goto , try/ catchpode causar bagunças em três dimensões , já que poderia subir na pilha não apenas para outras partes do mesmo método, mas também para outras classes e métodos , tudo isso será observado pelo desenvolvedor da manutenção, da maneira mais difícil ! No entanto, somos informados de que "goto é perigoso", heh!

No final, menciono que try/ catchtem sua vantagem, que é projetada para desativar otimizações ! É, se você quiser, um auxiliar de depuração ! É para isso que foi projetado e é para isso que deve ser usado ...

Acho que isso também é um ponto positivo. Ele pode ser usado para desabilitar otimizações que podem, de outra forma, paralisar algoritmos de transmissão de mensagens sãos e seguros para aplicativos multithread e para capturar possíveis condições de corrida;) Esse é o único cenário que consigo pensar para usar try / catch. Mesmo isso tem alternativas.


O que otimizações fazer try, catche finallydesativar?

AKA

Como são try, catche finallyútil como depuração ajudas?

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

12.3.3.13 Declarações try-catch

Para obter uma declaração stmt da forma:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • O estado de atribuição definida de v no início do bloco try é o mesmo que o estado de atribuição definida 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 do bloco try e cada catch-block-i (para cada i de 1 a n )

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

  • todas as atribuições feitas a objetos visíveis antes de entrar no try instrução devem ser concluídas, o que requer um bloqueio de thread para iniciar, tornando-o útil para depurar condições de corrida!
  • o compilador não tem permissão para:
    • eliminar atribuições de variáveis ​​não utilizadas que foram definitivamente atribuídas antes do try declaração
    • reorganizar ou aglutinar qualquer uma de suas atribuições internas (ou seja, veja meu primeiro link, se ainda não tiver feito isso).
    • içar atribuições sobre esta barreira, para atrasar a atribuição a uma variável que ele sabe que não será usada até mais tarde (se em tudo) ou para mover preventivamente atribuições posteriores para a frente para fazer outras otimizações possíveis ...

Uma história semelhante se aplica a cada catchafirmação; suponha que dentro de sua tryinstrução (ou um construtor ou função que ele invoca, etc) você atribua a essa variável sem sentido (digamos, garbage=42;), o compilador não pode eliminar essa instrução, não importa o quão irrelevante seja para o comportamento observável do programa . A atribuição precisa ser concluída antes que o catchbloco seja inserido.

Por que vale a pena, finallyconta uma história igualmente degradante :

12.3.3.14 Declarações try-finally

Para uma declaração try stmt do formulário:

try try-block
finally finally-block

• O estado de atribuição definida de v no início do bloco try é o mesmo que o estado de atribuição definida de v no início de stmt .
• O estado de atribuição definida de v no início do bloco final é o mesmo que o estado de atribuição definida 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): o v é definitivamente atribuído no ponto final do bloco try o vé definitivamente atribuído no ponto final do bloco final Se uma transferência de fluxo de controle (como uma instrução goto ) é feita que começa dentro do bloco try , e termina fora do bloco try , então v também é considerado definitivamente atribuído naquele controlar a transferência de fluxo se v for definitivamente atribuído no ponto final do bloco final . (Este não é um somente se - se v for definitivamente atribuído por outro motivo nesta transferência de fluxo de controle, então ainda é considerado definitivamente atribuído.)

12.3.3.15 Instruções try-catch-finally

Análise de atribuição definida para uma declaração try - catch - finally do formulário:

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

é feito como se a instrução fosse uma instrução try - finally envolvendo uma instrução try - catch :

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

Sem falar que se estiver dentro de um método frequentemente chamado, pode afetar o comportamento geral da aplicação.
Por exemplo, considero o uso de Int32.Parse uma má prática na maioria dos casos, uma vez que lança exceções para algo que pode ser facilmente detectado de outra forma.

Portanto, para concluir tudo o que está 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á-los.

dotmad
fonte
3

Eu escrevi um artigo sobre isso um tempo atrás porque havia muitas pessoas perguntando sobre isso na época. Você pode encontrá-lo e o código de teste em http://www.blackwasp.co.uk/SpeedTestExperimenteCatch.aspx .

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

O principal problema de desempenho com 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) reconhece a pausa como uma exceção que está prestes a acontecer! A chave aqui é não usar tratamento de exceção para operações normais. Como o nome sugere, eles são excepcionais e você deve fazer todo o possível para evitar que sejam atirados. Você não deve usá-los como parte do fluxo esperado de um programa que está funcionando corretamente.

BlackWasp
fonte
2

Eu fiz uma entrada no blog sobre esse assunto no ano passado. Confira. O ponto principal é que quase não há custo para um bloco try se nenhuma exceção ocorrer - e no meu laptop, uma exceção era de cerca de 36 μs. Isso pode ser menos do que você esperava, mas tenha em mente que esses resultados foram em uma pilha superficial. Além disso, as primeiras exceções são muito lentas.

Hafthor
fonte
Não consegui acessar seu blog (a conexão está expirando; você está usando try/ catchdemais? Heh heh), mas parece que você está discutindo com a especificação do idioma e alguns MS MVPs que também escreveram blogs sobre o assunto, fornecendo medidas ao contrário do seu conselho ... Estou aberto à sugestão de que a pesquisa que fiz está errada, mas vou precisar ler a entrada do seu blog para ver o que diz.
autista de
Além da postagem do blog de @Hafthor, aqui está outra postagem do blog com um código escrito especificamente para testar as diferenças de desempenho de velocidade. De acordo com os resultados, se ocorrer uma exceção apenas 5% do tempo, o código de Tratamento de Exceções será executado 100 vezes mais lento em geral do que o código de tratamento de não exceção. O artigo visa especificamente os métodos try-catchblock vs tryparse(), mas o conceito é o mesmo.
2

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

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

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


fonte
@Ritchard T, por quê? Em comparação com o programador e a sobrecarga de qualidade , é insignificante.
ThunderGr
1

Eu realmente gosto da postagem 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 fazer com que a CAMADA DE DADOS lance apenas um tipo de exceção (DataAccessException). Dessa forma, minha CAMADA DE NEGÓCIOS sabe que exceção esperar e a captura. Então, dependendo de outras regras de negócios (ou seja, se meu objeto de negócios participa do fluxo de trabalho, etc.), posso lançar uma nova exceção (BusinessObjectException) ou prosseguir sem relançar / lançar.

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

Por exemplo, este método participa de um fluxo 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;
}

fonte
2
David, você encerraria a chamada para 'DeleteGallery' em um bloco try / catch?
Rob
Como DeleteGallery é uma função booleana, parece-me que lançar uma exceção nela não é útil. Isso exigiria que a chamada para DeleteGallery fosse incluída em um bloco try / catch. Um if (! DeleteGallery (theid)) {// identificador} parece mais significativo para mim. nesse exemplo específico.
ThunderGr
1

Podemos ler em Programming Languages ​​Pragmatics de Michael L. Scott que os compiladores de hoje em dia não adicionam nenhum overhead no caso comum, ou seja, quando não ocorrem exceções. Portanto, todo trabalho é feito em tempo de compilação. Mas quando uma exceção é lançada em tempo de execução, o compilador precisa realizar uma pesquisa binária para encontrar a exceção correta e isso acontecerá para cada novo lançamento que você fizer.

Mas exceções são exceções e esse custo é perfeitamente aceitável. Se você tentar fazer o Tratamento de Exceções 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 montagem, que serão executadas toda vez que você entrar em suas sub-rotinas.

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

Vando
fonte
O compilador está fora de cena em tempo de execução. Não tem que ser uma sobrecarga para blocos try / catch para que o CLR pode lidar com as exceções. C # é executado no .NET CLR (uma máquina virtual). Parece-me que a sobrecarga do próprio bloco é mínima quando não há exceção, mas o custo do CLR tratando da exceção é muito significativo.
ThunderGr
-4

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

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

E aqui está aquele 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 essas duas partes equivalentes de código têm quase exatamente o mesmo comprimento em bytes. O último contém 4 bytes menos indentação. Isso é uma coisa ruim?

Para piorar a situação, o aluno decide fazer um loop enquanto a entrada pode ser analisada como um inteiro. A solução sem try / catch pode ser algo como:

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

Mas como isso fica ao usar try / catch?

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

Os blocos Try / catch são maneiras mágicas de desperdiçar indentação, e ainda nem sabemos o motivo do fracasso! Imagine como a pessoa que está fazendo a depuração se sente, quando o código continua a ser executado após uma falha lógica séria, em vez de parar com um erro de exceção óbvio. Os blocos Try / catch são validação / saneamento de dados de um preguiçoso.

Um dos custos menores é 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 . Acho que isso também é um ponto positivo. Ele pode ser usado para desabilitar otimizações que podem, de outra forma, paralisar algoritmos de transmissão de mensagens sãos e seguros para aplicativos multithread e para capturar possíveis condições de corrida;) Esse é o único cenário que consigo pensar para usar try / catch. Mesmo isso tem alternativas.

Sebastian ramadan
fonte
1
Tenho certeza de que o TryParse faz uma tentativa {int x = int.Parse ("xxx"); return true;} catch {return false; } internamente. A indentação não é uma preocupação na questão, apenas o desempenho e a sobrecarga.
ThunderGr
@ThunderGr Como alternativa, leia a nova resposta que postei. Ele contém mais links, um dos quais é uma análise do enorme aumento de desempenho quando você evita a Int.Parsefavor de Int.TryParse.
autista de