Os blocos try / catch prejudicam o desempenho quando exceções não são lançadas?

274

Durante uma revisão de código com um funcionário da Microsoft, encontramos uma grande seção de código dentro de um try{}bloco. Ela e um representante de TI sugeriram que isso pode ter efeitos no desempenho do código. De fato, eles sugeriram que a maior parte do código deveria estar fora dos blocos try / catch e que somente seções importantes deveriam ser verificadas. O funcionário da Microsoft acrescentou e disse que um próximo informe técnico adverte contra blocos de tentativa / captura incorretos.

Eu olhei em volta e descobri que isso pode afetar as otimizações , mas parece se aplicar apenas quando uma variável é compartilhada entre escopos.

Não estou perguntando sobre a manutenção do código, ou mesmo sobre as exceções corretas (o código em questão precisa ser refatorado, sem dúvida). Também não estou me referindo ao uso de exceções para controle de fluxo, isso é claramente errado na maioria dos casos. Essas são questões importantes (algumas são mais importantes), mas não o foco aqui.

Como os blocos try / catch afetam o desempenho quando exceções não são lançadas?

Kobi
fonte
147
"Aquele que sacrificaria a correção pelo desempenho também não merece".
Joel Coehoorn
16
Dito isto, a correção nem sempre precisa ser sacrificada pelo desempenho.
Dan Davies Brackett
19
Que tal curiosidade simples?
Samantha Branham
63
@ Joel: Talvez Kobi só queira saber a resposta por curiosidade. Saber se o desempenho será melhor ou pior não significa necessariamente que ele fará algo louco com seu código. A busca do conhecimento não é uma coisa boa?
LukeH
6
Aqui está um bom algoritmo para saber se você deve fazer essa alteração ou não. Primeiro, defina metas significativas de desempenho com base no cliente. Segundo, escreva o código para estar correto e claro primeiro. Terceiro, teste-o contra seus objetivos. Quarto, se você atingir seus objetivos, pare o trabalho mais cedo e vá à praia. Quinto, se você não atingir seus objetivos, use um criador de perfil para encontrar o código que é muito lento. Sexto, se esse código estiver muito lento por causa de um manipulador de exceções desnecessário, somente remova o manipulador de exceções. Caso contrário, corrija o código que é realmente muito lento. Volte para a etapa três.
Eric Lippert

Respostas:

203

Verifique-o.

static public void Main(string[] args)
{
    Stopwatch w = new Stopwatch();
    double d = 0;

    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(1);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
    w.Reset();
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(1);
    }

    w.Stop();
    Console.WriteLine(w.Elapsed);
}

Resultado:

00:00:00.4269033  // with try/catch
00:00:00.4260383  // without.

Em milissegundos:

449
416

Novo Código:

for (int j = 0; j < 10; j++)
{
    Stopwatch w = new Stopwatch();
    double d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        try
        {
            d = Math.Sin(d);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }

        finally
        {
            d = Math.Sin(d);
        }
    }

    w.Stop();
    Console.Write("   try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    w.Reset();
    d = 0;
    w.Start();

    for (int i = 0; i < 10000000; i++)
    {
        d = Math.Sin(d);
        d = Math.Sin(d);
    }

    w.Stop();
    Console.Write("No try/catch/finally: ");
    Console.WriteLine(w.ElapsedMilliseconds);
    Console.WriteLine();
}

Novos resultados:

   try/catch/finally: 382
No try/catch/finally: 332

   try/catch/finally: 375
No try/catch/finally: 332

   try/catch/finally: 376
No try/catch/finally: 333

   try/catch/finally: 375
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 329

   try/catch/finally: 373
No try/catch/finally: 330

   try/catch/finally: 373
No try/catch/finally: 352

   try/catch/finally: 374
No try/catch/finally: 331

   try/catch/finally: 380
No try/catch/finally: 329

   try/catch/finally: 374
No try/catch/finally: 334
Ben M
fonte
24
Você pode experimentá-los na ordem inversa e também para ter certeza de que a compilação do JIT não afetou a primeira?
JoshJordan
28
Programas como esse dificilmente parecem bons candidatos para testar o impacto do tratamento de exceções; muito do que estaria acontecendo em blocos normais de tentativa {} catch {} será otimizado. Eu posso ser para almoçar em que ...
LorenVS
30
Esta é uma compilação de depuração. O JIT não os otimiza.
217 Ben Ben
7
Isso não é verdade, pense nisso. Quantas vezes você usa tentar capturar em um loop? Na maioria das vezes, você usará o loop em um try.c
Athiwat Chunlakhan
9
Realmente? "Como os blocos try / catch afetam o desempenho quando exceções não são lançadas?"
317 Ben Ben
105

Depois de ver todas as estatísticas de try / catch e sem try / catch, a curiosidade me forçou a olhar para trás para ver o que é gerado para os dois casos. Aqui está o código:

C #:

private static void TestWithoutTryCatch(){
    Console.WriteLine("SIN(1) = {0} - No Try/Catch", Math.Sin(1)); 
}

MSIL:

.method private hidebysig static void  TestWithoutTryCatch() cil managed
{
  // Code size       32 (0x20)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "SIN(1) = {0} - No Try/Catch"
  IL_0006:  ldc.r8     1.
  IL_000f:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0014:  box        [mscorlib]System.Double
  IL_0019:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object)
  IL_001e:  nop
  IL_001f:  ret
} // end of method Program::TestWithoutTryCatch

C #:

private static void TestWithTryCatch(){
    try{
        Console.WriteLine("SIN(1) = {0}", Math.Sin(1)); 
    }
    catch (Exception ex){
        Console.WriteLine(ex);
    }
}

MSIL:

.method private hidebysig static void  TestWithTryCatch() cil managed
{
  // Code size       49 (0x31)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Exception ex)
  IL_0000:  nop
  .try
  {
    IL_0001:  nop
    IL_0002:  ldstr      "SIN(1) = {0}"
    IL_0007:  ldc.r8     1.
    IL_0010:  call       float64 [mscorlib]System.Math::Sin(float64)
    IL_0015:  box        [mscorlib]System.Double
    IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_001f:  nop
    IL_0020:  nop
    IL_0021:  leave.s    IL_002f //JUMP IF NO EXCEPTION
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_0023:  stloc.0
    IL_0024:  nop
    IL_0025:  ldloc.0
    IL_0026:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_002b:  nop
    IL_002c:  nop
    IL_002d:  leave.s    IL_002f
  }  // end handler
  IL_002f:  nop
  IL_0030:  ret
} // end of method Program::TestWithTryCatch

Eu não sou especialista em IL, mas podemos ver que um objeto de exceção local é criado na quarta linha .locals init ([0] class [mscorlib]System.Exception ex)depois que as coisas são praticamente as mesmas do método sem try / catch até a linha dezessete IL_0021: leave.s IL_002f. Se ocorrer uma exceção, o controle saltará para a linha, IL_0025: ldloc.0caso contrário, saltaremos para o rótulo IL_002d: leave.s IL_002fe a função retorna.

Posso assumir com segurança que, se nenhuma exceção ocorrer, será a sobrecarga da criação de variáveis ​​locais para armazenar apenas objetos de exceção e uma instrução de salto.

TheVillageIdiot
fonte
33
Bem, a IL inclui um bloco try / catch na mesma notação que em C #, então isso realmente não mostra o quanto sobrecarga um try / catch significa nos bastidores! Só que o IL não adiciona muito mais, não significa o mesmo que não foi adicionado algo no código de montagem compilado. A IL é apenas uma representação comum de todas as linguagens .NET. NÃO é um código de máquina!
temor
64

Não. Se as otimizações triviais que um bloco try / finalmente impedem realmente têm um impacto mensurável em seu programa, você provavelmente não deve usar o .NET em primeiro lugar.

John Kugelman
fonte
10
Esse é um ponto excelente - compare com os outros itens da nossa lista, este deve ser minúsculo. Devemos confiar nos recursos básicos da linguagem para se comportar corretamente e otimizar o que podemos controlar (sql, índices, algoritmos).
Kobi
3
Pense em laços apertados, companheiro. Exemplo do loop em que você lê e desserializa objetos de um fluxo de dados de soquete no servidor do jogo e está tentando espremer o máximo possível. Então você envia um MessagePack para serialização de objetos em vez de um formato binário e usa ArrayPool <byte> em vez de apenas criar matrizes de bytes, etc. Algumas otimizações serão ignoradas pelo compilador e a variável de exceção vai para o Gen0 GC. Tudo o que estou dizendo é que existem cenários "Alguns" em que tudo tem impacto.
Tcwicks 21/05/19
35

Explicação bastante abrangente do modelo de exceção .NET.

Petiscos de desempenho de Rico Mariani: Custo de exceção: Quando jogar e quando não

O primeiro tipo de custo é o custo estático de se tratar de exceção no seu código. As exceções gerenciadas realmente são comparativamente bem aqui, o que significa que o custo estático pode ser muito menor do que o C ++. Por que é isso? Bem, o custo estático é realmente incorrido em dois tipos de locais: primeiro, os sites reais de try / finalmente / catch / throw onde há código para essas construções. Segundo, no código não gerenciado, há o custo oculto associado ao rastreamento de todos os objetos que devem ser destruídos no caso de uma exceção ser lançada. Há uma quantidade considerável de lógica de limpeza que deve estar presente, e a parte sorrateira é que mesmo o código que não '

Dmitriy Zaslavskiy:

Conforme nota de Chris Brumme: Há também um custo relacionado ao fato de alguma otimização não estar sendo realizada pelo JIT na presença de captura

arul
fonte
1
O problema do C ++ é que um pedaço muito grande da biblioteca padrão gera exceções. Não há nada opcional sobre eles. Você precisa projetar seus objetos com algum tipo de política de exceção e, depois de fazer isso, não haverá mais custos ocultos.
21730 David Thornley
As alegações de Rico Mariani estão completamente erradas para o C ++ nativo. "o custo estático pode ser muito menor do que o C ++" - Isso simplesmente não é verdade. No entanto, não sei ao certo qual foi o design do mecanismo de exceção em 2003 quando o artigo foi escrito. C ++ realmente não tem nenhum custo quando exceções não são lançadas, não importa quantos blocos try / catch você tenha e onde eles estejam.
BJovke
1
@BJovke C ++ "manipulação de exceção de custo zero" significa apenas que não há custo em tempo de execução quando as exceções não são lançadas, mas ainda há um custo de tamanho de código importante devido a todo o código de limpeza que chama destruidores em exceções. Além disso, embora não haja nenhum código específico de exceção sendo gerado no caminho normal do código, o custo ainda não é realmente zero, porque a possibilidade de exceções ainda restringe o otimizador (por exemplo, o material necessário em caso de exceção precisa permanecer em algum lugar -> valores podem ser descartados de forma menos agressiva -> alocação de registradores menos eficiente)
Daniel
24

A estrutura é diferente, no exemplo de Ben M . Ele será estendido acima do forloop interno, o que fará com que não seja uma boa comparação entre os dois casos.

A seguir, é mais preciso para comparação, onde todo o código a ser verificado (incluindo declaração de variável) está dentro do bloco Try / Catch:

        for (int j = 0; j < 10; j++)
        {
            Stopwatch w = new Stopwatch();
            w.Start();
            try { 
                double d1 = 0; 
                for (int i = 0; i < 10000000; i++) { 
                    d1 = Math.Sin(d1);
                    d1 = Math.Sin(d1); 
                } 
            }
            catch (Exception ex) {
                Console.WriteLine(ex.ToString()); 
            }
            finally { 
                //d1 = Math.Sin(d1); 
            }
            w.Stop(); 
            Console.Write("   try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            w.Reset(); 
            w.Start(); 
            double d2 = 0; 
            for (int i = 0; i < 10000000; i++) { 
                d2 = Math.Sin(d2);
                d2 = Math.Sin(d2); 
            } 
            w.Stop(); 
            Console.Write("No try/catch/finally: "); 
            Console.WriteLine(w.ElapsedMilliseconds); 
            Console.WriteLine();
        }

Quando executei o código de teste original de Ben M , notei uma diferença nas configurações Debug e Releas.

Nesta versão, notei uma diferença na versão de depuração (na verdade mais do que a outra versão), mas não houve diferença na versão de lançamento.

Conclution :
Com base nestes testes, acho que podemos dizer que try / catch faz ter um pequeno impacto no desempenho.

EDIT:
Tentei aumentar o valor do loop de 10000000 para 1000000000 e executei novamente no Release para obter algumas diferenças no release, e o resultado foi o seguinte:

   try/catch/finally: 509
No try/catch/finally: 486

   try/catch/finally: 479
No try/catch/finally: 511

   try/catch/finally: 475
No try/catch/finally: 477

   try/catch/finally: 477
No try/catch/finally: 475

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 477
No try/catch/finally: 474

   try/catch/finally: 475
No try/catch/finally: 475

   try/catch/finally: 476
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 476

   try/catch/finally: 475
No try/catch/finally: 474

Você vê que o resultado é inconseqüente. Em alguns casos, a versão usando Try / Catch é realmente mais rápida!

temor
fonte
1
Também notei isso, às vezes é mais rápido com o try / catch. Eu comentei na resposta de Ben. No entanto, diferentemente de 24 eleitores, não gosto desse tipo de benchmarking, não acho que seja uma boa indicação. O código é mais rápido nesse caso, mas será sempre?
Kobi
5
Isso não prova que sua máquina estava executando uma variedade de outras tarefas ao mesmo tempo? O tempo decorrido nunca é uma boa medida, é necessário usar um criador de perfil que registra o tempo do processador, não o tempo decorrido.
Colin Desmond
2
@Kobi: Eu concordo que essa não é a melhor maneira de avaliar se você deseja publicá-lo como uma prova de que seu programa roda mais rápido do que outro ou algo assim, mas pode fornecer a você como desenvolvedor uma indicação de um método com desempenho melhor que outro . Nesse caso, acho que podemos dizer que as diferenças (pelo menos para a configuração do Release) são ignoráveis.
temor
1
Você não está cronometrando try/catchaqui. Você está cronometrando 12 tentativas / capturas na seção crítica contra loops de 10 milhões. O ruído do loop irá erradicar qualquer influência que o try / catch tenha. se, em vez disso, você colocar o try / catch dentro do loop apertado e comparar com / sem, você acabaria com o custo do try / catch. (sem dúvida, essa codificação geralmente não é uma boa prática, mas se você deseja calcular a sobrecarga de uma construção, é assim que a faz). Atualmente, o BenchmarkDotNet é a ferramenta essencial para tempos de execução confiáveis.
Abel
15

Testei o impacto real de um try..catchcircuito fechado e ele é pequeno demais para ser uma preocupação de desempenho em qualquer situação normal.

Se o loop fizer muito pouco trabalho (no meu teste que fiz x++), você poderá medir o impacto do tratamento de exceções. O loop com manipulação de exceção demorou cerca de dez vezes mais para ser executado.

Se o loop executar algum trabalho real (no meu teste, chamei o método Int32.Parse), o tratamento de exceções terá muito pouco impacto para ser mensurável. Eu tenho uma diferença muito maior trocando a ordem dos loops ...

Guffa
fonte
11

Os blocos try catch têm um impacto insignificante no desempenho, mas a exceção Arremesso pode ser bastante considerável, provavelmente é aí que seu colega de trabalho ficou confuso.

RHicke
fonte
8

A tentativa / captura tem impacto no desempenho.

Mas não é um grande impacto. a complexidade try / catch é geralmente O (1), como uma atribuição simples, exceto quando são colocadas em um loop. Então você tem que usá-los com sabedoria.

Aqui está uma referência sobre o desempenho try / catch (embora não explique a complexidade, mas está implícito). Dê uma olhada na seção Lançar Menos Exceções

Isaac
fonte
3
Complexidade é O (1), não significa muito. Por exemplo, se você equipar uma seção de código chamada frequentemente com try-catch (ou mencionar um loop), os O (1) s poderão somar um número mensurável no final.
precisa saber é o seguinte
6

Em teoria, um bloco try / catch não terá efeito no comportamento do código, a menos que uma exceção realmente ocorra. Existem algumas circunstâncias raras, no entanto, em que a existência de um bloco de tentativa / captura pode ter um efeito importante, e outras incomuns, mas dificilmente obscuras, nas quais o efeito pode ser perceptível. A razão para isso é que o código fornecido, como:

Action q;
double thing1()
  { double total; for (int i=0; i<1000000; i++) total+=1.0/i; return total;}
double thing2()
  { q=null; return 1.0;}
...
x=thing1();     // statement1
x=thing2(x);    // statement2
doSomething(x); // statement3

o compilador pode otimizar a instrução1 com base no fato de que a instrução2 é executada antes da instrução3. Se o compilador puder reconhecer que o item1 não tem efeitos colaterais e o item2 não usa x, ele pode omitir completamente o item1. Se [como neste caso] a coisa1 for cara, isso pode ser uma grande otimização, embora os casos em que a coisa1 seja cara também sejam aqueles em que o compilador provavelmente menos otimizará. Suponha que o código tenha sido alterado:

x=thing1();      // statement1
try
{ x=thing2(x); } // statement2
catch { q(); }
doSomething(x);  // statement3

Agora existe uma sequência de eventos em que a instrução3 poderia ser executada sem a instrução2 ter sido executada. Mesmo que nada no código for thing2capaz de gerar uma exceção, seria possível que outro thread pudesse usar um Interlocked.CompareExchangepara perceber que qfoi limpo e configurá-lo como Thread.ResetAbort, e então executar uma Thread.Abort()instrução before2 que escreveu seu valor em x. Então o catchseria executado Thread.ResetAbort()[via delegado q], permitindo que a execução continuasse com a instrução3. É claro que essa sequência de eventos seria excepcionalmente improvável, mas é necessário um compilador para gerar código que funcione de acordo com as especificações, mesmo quando esses eventos improváveis ​​ocorrem.

Em geral, é muito mais provável que o compilador perceba oportunidades de deixar de fora pedaços simples de código que códigos complexos e, portanto, seria raro uma tentativa / captura afetar o desempenho muito se exceções nunca forem lançadas. Ainda assim, existem algumas situações em que a existência de um bloco try / catch pode impedir otimizações que - mas para o try / catch - teriam permitido que o código fosse executado mais rapidamente.

supercat
fonte
5

Embora " Prevenir é melhor que manipular ", na perspectiva de desempenho e eficiência, poderíamos escolher o try-catch sobre a pré-varicação. Considere o código abaixo:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    if (i != 0)
    {
        int k = 10 / i;
    }
}
stopwatch.Stop();
Console.WriteLine($"With Checking: {stopwatch.ElapsedMilliseconds}");
stopwatch.Reset();
stopwatch.Start();
for (int i = 1; i < int.MaxValue; i++)
{
    try
    {
        int k = 10 / i;
    }
    catch (Exception)
    {

    }
}
stopwatch.Stop();
Console.WriteLine($"With Exception: {stopwatch.ElapsedMilliseconds}");

Aqui está o resultado:

With Checking: 20367
With Exception: 13998
Ted Oddman
fonte
4

Consulte a discussão sobre a implementação try / catch para obter uma discussão sobre como os blocos try / catch funcionam e como algumas implementações têm sobrecarga alta e outras zero, quando nenhuma exceção ocorre. Em particular, acho que a implementação do Windows de 32 bits tem uma sobrecarga alta e a implementação de 64 bits não.

Ira Baxter
fonte
O que eu descrevi são duas abordagens diferentes para implementar exceções. As abordagens se aplicam igualmente ao C ++ e C #, bem como ao código gerenciado / não gerenciado. Quais os que a Microsoft escolheu para o seu C # não sei exatamente, mas a arquitetura de tratamento de exceções dos aplicativos em nível de máquina fornecidos pela MS usa o esquema mais rápido. Eu ficaria um pouco surpreso se a implementação de C # para 64 bits não a usasse.
Ira Baxter