Quão lenta são as exceções do .NET?

143

Não quero discutir sobre quando e não lançar exceções. Desejo resolver um problema simples. Em 99% do tempo, o argumento para não lançar exceções gira em torno de serem lentos, enquanto o outro lado afirma (com teste de benchmark) que a velocidade não é o problema. Eu li vários blogs, artigos e postagens de um lado ou de outro. Então qual é?

Alguns links das respostas: Skeet , Mariani , Brumme .

Goran
fonte
13
existem mentiras, malditas mentiras e referências. :)
gbjbaanb 02/10/08
Infelizmente, várias respostas com alta votação aqui falharam que a pergunta estava perguntando "quão lentas são as exceções?" E especificamente pedidas para evitar o tópico de quantas vezes usá-las. Uma resposta simples para a pergunta que, como realmente foi feita, é ..... No Windows CLR, as exceções são 750 vezes mais lentas que os valores retornados.
David Jeske

Respostas:

207

Estou do lado "não lento" - ou mais precisamente "não lento o suficiente para fazer valer a pena evitá-los em uso normal". Eu escrevi dois artigos curtos sobre isso. Existem críticas ao aspecto de benchmark, que são basicamente "na vida real, haveria mais pilha para passar, então você explodiria o cache etc." - mas o uso de códigos de erro para aumentar a pilha também explodir o cache, então não vejo isso como um argumento particularmente bom.

Só para esclarecer: não apoio o uso de exceções onde não são lógicas. Por exemplo, int.TryParseé totalmente apropriado para converter dados de um usuário. É apropriado ao ler um arquivo gerado por máquina, onde a falha significa "O arquivo não está no formato que deveria ser, eu realmente não quero tentar lidar com isso, pois não sei o que mais pode estar errado. "

Ao usar exceções em "apenas circunstâncias razoáveis", nunca vi um aplicativo cujo desempenho foi significativamente prejudicado por exceções. Basicamente, as exceções não devem ocorrer com freqüência, a menos que você tenha problemas significativos de correção, e se você tiver problemas significativos de correção, o desempenho não será o maior problema que você enfrenta.

Jon Skeet
fonte
2
exceções, infelizmente, as pessoas são contadas são livres, usá-los para a funcionalidade trivial 'correta', eles devem ser usados como você diz, quando as coisas têm errado gone - em circunstâncias 'excepcionais'
gbjbaanb
4
Sim, as pessoas certamente devem estar cientes de que há um custo de desempenho associado ao uso de exceções de forma inadequada. Eu só acho que é um não-problema quando eles são usados adequadamente :)
Jon Skeet
7
@PaulLockwood: Eu diria que, se você tiver mais de 200 exceções por segundo , estará abusando delas. Claramente, não é um evento "excepcional" se ocorrer 200 vezes por segundo. Observe a última frase da resposta: "Basicamente, as exceções não devem ocorrer com freqüência, a menos que você tenha problemas significativos de correção, e se você tiver problemas significativos de correção, o desempenho não é o maior problema que você enfrenta".
precisa
4
@PaulLockwood: Meu argumento é que, se você tiver mais de 200 exceções por segundo, isso provavelmente indica que você está abusando de exceções. Não me surpreende que esse seja o caso, mas significa que o aspecto do desempenho não seria minha primeira preocupação - o abuso de exceções seria. Depois de remover todos os usos inadequados das exceções, não esperaria que elas fossem uma parte significativa do desempenho.
precisa
4
@ DavidJeske: Você perdeu o ponto da resposta. Obviamente, lançar uma exceção é muito mais lento do que retornar um valor normal. Ninguém está discutindo isso. A questão é se eles são muito lentos. Se você estiver em uma situação adequada para lançar uma exceção e estiver causando um problema de desempenho, provavelmente terá problemas maiores - porque isso sugere que há muita coisa errada no seu sistema. Normalmente, o problema é realmente que você está usando exceções quando eles são inadequados para começar.
21412 Jon Skeet
31

Existe a resposta definitiva para isso do cara que os implementou - Chris Brumme. Ele escreveu um excelente artigo de blog sobre o assunto (aviso - é muito longo) (aviso2 - é muito bem escrito, se você é um técnico, você o lerá até o fim e depois terá que compensar suas horas depois do trabalho :) )

O resumo executivo: eles são lentos. Eles são implementados como exceções do Win32 SEH, portanto, alguns até passarão do limite da CPU do anel 0! Obviamente, no mundo real, você fará muitos outros trabalhos para que a exceção estranha não seja notada, mas se você usá-los para o fluxo do programa, espere que seu aplicativo seja martelado. Este é outro exemplo da máquina de marketing da MS fazendo um desserviço. Lembro-me de um microsoftie nos dizendo como eles incorriam em zero absolutamente zero, o que é algo totalmente diferente.

Chris faz uma citação pertinente:

De fato, o CLR usa internamente exceções, mesmo nas partes não gerenciadas do mecanismo. No entanto, há um sério problema de desempenho a longo prazo com exceções e isso deve ser levado em consideração na sua decisão.

gbjbaanb
fonte
Posso mencionar isso em testes do mundo real, onde um tipo anulável está causando uma exceção muitas vezes gerada em um "fluxo de programa normal", que acabou com problemas significativos de desempenho. Lembre-se sempre, as exceções são para casos excepcionais, não acredite em quem disser o contrário, ou você terminará com um thread do github como esse!
Gbjbaanb #
8

Não tenho idéia do que as pessoas estão falando quando dizem que são lentas apenas se forem jogadas.

Edição: Se exceções não são lançadas, isso significa que você está fazendo nova exceção () ou algo parecido. Caso contrário, a exceção fará com que o encadeamento seja suspenso e a pilha percorrida. Isso pode ser bom em situações menores, mas em sites de alto tráfego, depender de exceções como um mecanismo de fluxo de trabalho ou caminho de execução certamente causará problemas de desempenho. As exceções, por si só, não são ruins e são úteis para expressar condições excepcionais

O fluxo de trabalho de exceção em um aplicativo .NET usa exceções de primeira e segunda chance. Para todas as exceções, mesmo se você as estiver capturando e manipulando, o objeto de exceção ainda será criado e a estrutura ainda precisará percorrer a pilha para procurar um manipulador. Se você capturar e repetir novamente, é claro que isso levará mais tempo - você receberá uma exceção de primeira chance, capturá-la, reproduzi-la novamente, causando outra exceção de primeira chance, que não encontra um manipulador, o que causa uma exceção de segunda chance.

Exceções também são objetos no heap - portanto, se você estiver lançando várias exceções, estará causando problemas de desempenho e memória.

Além disso, de acordo com minha cópia de "Testes de desempenho Microsoft .NET Web Applications", escrita pela equipe da ACE:

"O tratamento de exceções é caro. A execução do encadeamento envolvido é suspensa enquanto o CLR recorre pela pilha de chamadas em busca do manipulador de exceções certo e, quando encontrado, o manipulador de exceções e algum número de blocos finalmente devem ter sua chance de executar antes que o processamento regular possa ser executado ".

Minha própria experiência no campo mostrou que a redução de exceções ajudou significativamente o desempenho. Obviamente, há outras coisas que você leva em consideração ao testar o desempenho - por exemplo, se a sua E / S de disco é filmada ou suas consultas são em segundos, esse deve ser seu foco. Mas encontrar e remover exceções deve ser uma parte vital dessa estratégia.

Cory Foy
fonte
1
Nada do que você escreveu contradiz a afirmação de que as exceções são lentas apenas se forem lançadas. Você só falou sobre situações em que elas são jogadas. Quando você "ajudou significativamente o desempenho" removendo exceções: 1) Elas eram verdadeiras condições de erro ou apenas erro do usuário ?
Jon Skeet
2) Você estava executando no depurador ou não?
Jon Skeet
A única coisa possível que você pode fazer com uma exceção, se não a lançar, é criá-la como um objeto, o que não faz sentido. Estar sob o depurador ou não, não importa - ainda será mais lento. Sim, existem ganchos que vão acontecer com um depurador anexado, mas ainda é lento
Cory Foy
4
Eu sei - eu fazia parte da equipe Premier da MSFT. :) Vamos apenas dizer, muitos - milhares por segundo em alguns casos extremos que vimos. Nada como conectar-se a um depurador ativo e apenas ver exceções o mais rápido que você pode ler. Ex são lentos - assim como a conexão com um banco de dados, e você faz isso quando faz sentido.
Cory Foy
5
Cory, acho que o ponto "apenas é lento quando jogados" é que você não precisa se preocupar com desempenho por causa da mera presença de blocos catch / finalmente. Ou seja, elas mesmas não causam um impacto no desempenho, apenas a ocorrência de uma instância de exceção real.
Ian Horwill
6

O argumento que eu entendo não é que lançar exceções seja ruim, elas são lentas por si só. Em vez disso, trata-se de usar a construção throw / catch como uma maneira de primeira classe de controlar a lógica normal do aplicativo, em vez das construções condicionais mais tradicionais.

Freqüentemente, na lógica normal do aplicativo, você executa um loop em que a mesma ação é repetida milhares / milhões de vezes. Nesse caso, com alguns perfis muito simples (consulte a classe Cronômetro), você pode ver por si mesmo que lançando uma exceção em vez de dizer uma declaração if simples pode ser substancialmente mais lenta.

De fato, li uma vez que a equipe .NET da Microsoft introduziu os métodos TryXXXXX no .NET 2.0 para muitos dos tipos básicos de FCL, especificamente porque os clientes estavam reclamando que o desempenho de seus aplicativos era muito lento.

Acontece que, em muitos casos, isso ocorreu porque os clientes estavam tentando a conversão de valores de tipo em um loop e cada tentativa falhou. Uma exceção de conversão foi lançada e capturada por um manipulador de exceções que engoliu a exceção e continuou o loop.

A Microsoft agora recomenda que os métodos TryXXX sejam usados ​​particularmente nessa situação para evitar possíveis problemas de desempenho.

Eu posso estar errado, mas parece que você não tem certeza da veracidade dos "parâmetros de referência" sobre os quais você leu. Solução simples: Experimente você mesmo.

Cinza
fonte
Eu pensei que internamente essas funções "try" também usam exceções?
greg
1
Essas funções "Try" não lançam exceções internamente por uma falha ao analisar o valor de entrada. No entanto, eles ainda lançam exceções para outras situações de erro, como ArgumentException.
Ash
Penso que esta resposta se aproxima do cerne da questão do que qualquer outra. Dizer 'usar exceções apenas em circunstâncias razoáveis' não responde realmente à pergunta - a verdadeira percepção é que o uso de exceções em C # para fluxo de controle é muito mais lento que as construções condicionais usuais. Você poderia ser perdoado por pensar de outra forma. No OCaml, as exceções são mais ou menos um GOTO e a maneira aceita de implementar interrupções ao usar os recursos imperativos. No meu caso particular, a substituição em um loop apertado int.Parse () mais try / catch vs. int.TryParse () deu um aumento significativo no desempenho.
Hugh W
4

Meu servidor XMPP ganhou um grande aumento de velocidade (desculpe, não há números reais, puramente observacional) depois de tentar consistentemente impedir que eles aconteçam (como verificar se um soquete está conectado antes de tentar ler mais dados) e me oferecer maneiras de evitá-los (os métodos TryX mencionados). Isso ocorreu com apenas cerca de 50 usuários virtuais ativos (conversando).

Jonathan C Dickinson
fonte
3
Números seria útil, infelizmente :( Coisas como operações de socket devem compensar amplamente os custos de exceção, certamente, quando não depuração Se você sempre referência-lo totalmente, eu estaria realmente interessado em ver os resultados..
Jon Skeet
3

Apenas para adicionar minha própria experiência recente a essa discussão: de acordo com a maior parte do que foi escrito acima, achei as exceções de lançamento extremamente lentas quando feitas repetidamente, mesmo sem o depurador em execução. Acabei de aumentar o desempenho de um programa grande que estou escrevendo em 60%, alterando cerca de cinco linhas de código: alternando para um modelo de código de retorno em vez de lançar exceções. Concedido, o código em questão estava sendo executado milhares de vezes e potencialmente lançava milhares de exceções antes de eu o alterar. Portanto, concordo com a afirmação acima: lance exceções quando algo importante realmente der errado, não como uma maneira de controlar o fluxo de aplicativos em qualquer situação "esperada".

Ray Prisament
fonte
2

Se você compará-los para retornar códigos, eles são lentos como o inferno. No entanto, como os cartazes anteriores declararam, você não deseja executar a operação normal do programa, portanto, você obtém o desempenho perfeito quando ocorre um problema e, na grande maioria dos casos, o desempenho não importa mais (pois a exceção implica um obstáculo).

Definitivamente vale a pena usar códigos de erro, as vantagens são vastas na IMO.

Quibblesome
fonte
2

Eu nunca tive nenhum problema de desempenho com exceções. Eu uso muito exceções - nunca uso códigos de retorno, se puder. Eles são uma prática ruim e, na minha opinião, cheiram a código de espaguete.

Eu acho que tudo se resume a como você usa exceções: se você usá-las como códigos de retorno (cada chamada de método na pilha captura e repete), sim, elas serão lentas, porque você sobrecarregará cada captura / lançamento.

Mas se você joga no fundo da pilha e pega no topo (você substitui toda uma cadeia de códigos de retorno por um lançamento / captura), todas as operações caras são feitas uma vez.

No final do dia, eles são um recurso de idioma válido.

Só para provar meu ponto

Execute o código neste link (grande demais para uma resposta).

Resultados no meu computador:

marco@sklivvz:~/develop/test$ mono Exceptions.exe | grep PM
10/2/2008 2:53:32 PM
10/2/2008 2:53:42 PM
10/2/2008 2:53:52 PM

Os registros de data e hora são emitidos no início, entre códigos de retorno e exceções, no final. Leva o mesmo tempo em ambos os casos. Observe que você precisa compilar com otimizações.

Sklivvz
fonte
2

Mas o mono lança a exceção 10x mais rápido que o modo independente de .net e o modo independente de .net lança a exceção 60x mais rápido que o modo de depurador .net (As máquinas de teste têm o mesmo modelo de CPU)

int c = 1000000;
int s = Environment.TickCount;
for (int i = 0; i < c; i++)
{
    try { throw new Exception(); }
    catch { }
}
int d = Environment.TickCount - s;

Console.WriteLine(d + "ms / " + c + " exceptions");
ceder
fonte
1

No modo de liberação, a sobrecarga é mínima.

A menos que você esteja usando exceções para controle de fluxo (por exemplo, saídas não locais) de forma recursiva, duvido que você consiga perceber a diferença.

leppie
fonte
1

No Windows CLR, para uma cadeia de chamadas com profundidade 8, lançar uma exceção é 750 vezes mais lento do que verificar e propagar um valor de retorno. (veja abaixo os benchmarks)

Esse alto custo para exceções ocorre porque o Windows CLR se integra a algo chamado Windows Structured Exception Handling . Isso permite que as exceções sejam capturadas e lançadas corretamente em diferentes tempos de execução e idiomas. No entanto, é muito, muito lento.

Exceções no tempo de execução Mono (em qualquer plataforma) são muito mais rápidas, porque não se integra ao SEH. No entanto, há perda de funcionalidade ao passar exceções em vários tempos de execução, porque ele não usa nada como o SEH.

Aqui estão os resultados abreviados do meu benchmark de exceções versus valores de retorno para o Windows CLR.

baseline: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.25 (0), time elapsed 13.0007 ms
baseline: recurse_depth 8, error_freqeuncy 0.5 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 0.75 (0), time elapsed 13.0008 ms
baseline: recurse_depth 8, error_freqeuncy 1 (0), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0 (0), time elapsed 13.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.25 (249999), time elapsed 14.0008 ms
retval_error: recurse_depth 5, error_freqeuncy 0.5 (499999), time elapsed 16.0009 ms
retval_error: recurse_depth 5, error_freqeuncy 0.75 (999999), time elapsed 16.001 ms
retval_error: recurse_depth 5, error_freqeuncy 1 (999999), time elapsed 16.0009 ms
retval_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 20.0011 ms
retval_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 21.0012 ms
retval_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 24.0014 ms
retval_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 24.0013 ms
exception_error: recurse_depth 8, error_freqeuncy 0 (0), time elapsed 31.0017 ms
exception_error: recurse_depth 8, error_freqeuncy 0.25 (249999), time elapsed 5607.3208     ms
exception_error: recurse_depth 8, error_freqeuncy 0.5 (499999), time elapsed 11172.639  ms
exception_error: recurse_depth 8, error_freqeuncy 0.75 (999999), time elapsed 22297.2753 ms
exception_error: recurse_depth 8, error_freqeuncy 1 (999999), time elapsed 22102.2641 ms

E aqui está o código ..

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

public class TestIt {
    int value;

    public class TestException : Exception { } 

    public int getValue() {
        return value;
    }

    public void reset() {
        value = 0;
    }

    public bool baseline_null(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            return shouldfail;
        } else {
            return baseline_null(shouldfail,recurse_depth-1);
        }
    }

    public bool retval_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                return false;
            } else {
                return true;
            }
        } else {
            bool nested_error = retval_error(shouldfail,recurse_depth-1);
            if (nested_error) {
                return true;
            } else {
                return false;
            }
        }
    }

    public void exception_error(bool shouldfail, int recurse_depth) {
        if (recurse_depth <= 0) {
            if (shouldfail) {
                throw new TestException();
            }
        } else {
            exception_error(shouldfail,recurse_depth-1);
        }

    }

    public static void Main(String[] args) {
        int i;
        long l;
        TestIt t = new TestIt();
        int failures;

        int ITERATION_COUNT = 1000000;


        // (0) baseline null workload
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    t.baseline_null(shoulderror,recurse_depth);
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "baseline: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }


        // (1) retval_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    if (!t.retval_error(shoulderror,recurse_depth)) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "retval_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));
            }
        }

        // (2) exception_error
        for (int recurse_depth = 2; recurse_depth <= 10; recurse_depth+=3) {
            for (float exception_freq = 0.0f; exception_freq <= 1.0f; exception_freq += 0.25f) {            
                int EXCEPTION_MOD = (exception_freq == 0.0f) ? ITERATION_COUNT+1 : (int)(1.0f / exception_freq);            

                failures = 0;
                DateTime start_time = DateTime.Now;
                t.reset();              
                for (i = 1; i < ITERATION_COUNT; i++) {
                    bool shoulderror = (i % EXCEPTION_MOD) == 0;
                    try {
                        t.exception_error(shoulderror,recurse_depth);
                    } catch (TestException e) {
                        failures++;
                    }
                }
                double elapsed_time = (DateTime.Now - start_time).TotalMilliseconds;
                Console.WriteLine(
                    String.Format(
                      "exception_error: recurse_depth {0}, error_freqeuncy {1} ({2}), time elapsed {3} ms",
                        recurse_depth, exception_freq, failures,elapsed_time));         }
        }
    }
}


}
David Jeske
fonte
5
Além de perder o objetivo da pergunta, não use o DateTime. Agora, para os benchmarks - use o Cronômetro, projetado para medir o tempo decorrido. Não deve ser um problema aqui, pois você mede períodos de tempo razoavelmente longos, mas vale a pena adquirir esse hábito.
Jon Skeet
Pelo contrário, a questão é "são exceções lentas", ponto final. Ele pediu especificamente para evitar o tópico de quando lançar exceções, porque esse tópico oculta os fatos. Qual é o desempenho das exceções?
David Jeske
0

Uma observação rápida aqui sobre o desempenho associado à captura de exceções.

Quando o caminho da execução entra em um bloco 'try', nada de mágico acontece. Não há instruções 'try' e nenhum custo associado à entrada ou saída do bloco try. As informações sobre o bloco try são armazenadas nos metadados do método e esses metadados são usados ​​em tempo de execução sempre que uma exceção é gerada. O mecanismo de execução percorre a pilha procurando a primeira chamada que estava contida em um bloco try. Qualquer sobrecarga associada ao tratamento de exceções ocorre apenas quando as exceções são lançadas.

Drew Noakes
fonte
1
No entanto, a presença de exceções pode afetar a otimização - métodos com manipuladores de exceção explícitos são mais difíceis de alinhar e a reordenação de instruções é limitada por eles.
Eamon Nerbonne
-1

Ao escrever classes / funções para outras pessoas, parece difícil dizer quando as exceções são apropriadas. Há algumas partes úteis da BCL que eu tive que abandonar e usar o pinvoke porque elas lançam exceções em vez de retornar erros. Em alguns casos, você pode contornar esse problema, mas para outros, como System.Management and Performance Counters, existem usos em que você precisa executar loops nos quais as exceções são lançadas pela BCL com freqüência.

Se você estiver gravando uma biblioteca e houver uma possibilidade remota de que sua função possa ser usada em um loop e houver um potencial para uma grande quantidade de iterações, use o padrão Try .. ou alguma outra maneira de expor os erros, além das exceções. E mesmo assim, é difícil dizer quanto sua função será chamada se estiver sendo usada por muitos processos em ambiente compartilhado.

No meu próprio código, exceções são lançadas apenas quando as coisas são tão excepcionais que é necessário examinar o rastreamento de pilha e ver o que deu errado e corrigi-lo. Então, eu praticamente reescrevi partes do BCL para usar o tratamento de erros com base no padrão Try .. em vez de exceções.

Covarde anônimo
fonte
2
Isso parece não se encaixar na declaração " Não quero discutir sobre quando e não lançar exceções ".
Hrbrmstr 23/03