Está rolando uma discussão no .Net Architects sobre o custo das exceções. Eu sempre soube que elas eram muito caras. O Philip Calçado colocava que não lançar exceções por questões de performance é otimização prematura. Bom, fui checar.

Fiz um código em que criava uma entidade inválida. Em um, a entidade era criada inválida e um valor de status de invalidação era configurado. No outro a entidade não foi criada, e uma exceção foi lançada no construtor.

Vejam as entidades:

class ComExcecao
{
    public ComExcecao(bool algumParam)
    {
        throw new Exception("erro");
    }
}

class SemExcecao
{
    public SemExcecao(bool algumParam)
    {
        EhValida = false;
    }
    public bool EhValida { get; set; }
}

Aqui está o código para testar isso:

static void Main(string[] args)
{
    var quantidadeDeVezes = 100000;
    Console.WriteLine("Quantas vezes você quer rodar? Padrão == {0}", quantidadeDeVezes);
    var vezesInputada = Console.ReadLine();
    
    EngolirErro(()=>quantidadeDeVezes = Convert.ToInt32(vezesInputada));

    Console.WriteLine("Rodando {0} iterações...", quantidadeDeVezes);

    var totalSemExcecao =
        Executar(() =>
                     {
                         for (int i = 0; i < quantidadeDeVezes; i++)
                         {
                             bool EhValida;
                             var classe = new SemExcecao(true);
                             EhValida = classe.EhValida;
                         }
                     });
    Console.WriteLine("Total sem exceção: {0} milisegundos", totalSemExcecao);

    var totalComExcecao =
        Executar(() =>
                     {
                         for (int i = 0; i < quantidadeDeVezes; i++)
                         {
                             bool EhValida;
                             try
                             {
                                 new ComExcecao(true);
                                 EhValida = true;
                             }
                             catch (Exception)
                             {
                                 EhValida = false;
                             }
                         }
                     });
    Console.WriteLine("Total com exceção: {0} milisegundos", totalComExcecao);

    var diferenca = Math.Abs(totalComExcecao - totalSemExcecao);
    var maisRapido = totalComExcecao > totalSemExcecao ? "sem exceção" : "com exceção";
    var percentualMaisRapido = ObterPercentualMaior(totalComExcecao, totalSemExcecao);
    
    Console.WriteLine("Código {0} é mais rápido em {1} milisegundos ({2}).", 
        maisRapido, 
        diferenca,
        percentualMaisRapido);

    Console.ReadLine();

}

Estou ocultando as funções de auxílio para não poluir (no final eu mostro).

Resultados (código compilado como release, cliquem nas figuras para ampliar):

  • Dez milhões de iterações

    Total sem exceção: 112 milisegundos (um décimo de um segundo)

    Total com exceção: 256.310 milisegundos (256 segundos, ou quase 5 minutos)

    Código sem exceção é mais rápido em 256.198 milisegundos (2288.48%).

    Cem milhões 

  • Um milhão de iterações:

    Total sem exceção: 9 milisegundos

    Total com exceção: 25.286 milisegundos

    Código sem exceção é mais rápido em 25.277 milisegundos (2809.56%).

    Um milhão

  • Cem mil iterações:

    Total sem exceção: 2 milisegundos

    Total com exceção: 2.497 milisegundos

    Código sem exceção é mais rápido em 2.495 milisegundos (1248.50%).

    Cem mil

  • Dez mil iterações:

    Total sem exceção: 0 milisegundos

    Total com exceção: 252 milisegundos

    Código sem exceção é mais rápido em 252 milisegundos (Menor valor == 0).

    Dez mil 

Minha avaliação dos resultados é que não dá mesmo pra colocar exceções onde código de negócio devia estar ocorrendo. Como eu já havia dito aqui antes, exceções são coisas que não são esperadas (daí o nome “exceção”), como erros, quedas de conexão, etc. Se a magnitude da diferença fosse de 200% até conseguiria entender isso como otimização prematura. Mas uma diferença de quase 3 mil porcento significa que em vez de um servidor, vou ter que colocar 30 pra fazer a mesma coisa. Não faz o menor sentido em praticamente nenhum cenário.

Mais algumas fontes:

  1. Exceptions and Performance e Exceptions and Performance Redux
  2. The True Cost of .NET Exceptions e The True Cost of .NET Exceptions — Solution

Pra quem ficou curioso, segue o código que faz a avaliação do tempo, ele usa uma classezinha pouco utilizada do Framework, a System.Diagnostics.StopWatch:

public static double Executar(Action acao)
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    acao();
    stopwatch.Stop();
    return stopwatch.ElapsedMilliseconds;            
}

O código, para quem quiser refazer os testes, está aqui.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.