Sempre que falamos em performance provavelmente o tópico mais pertinente a ser colocado em pauta será o volume, por exemplo:
Sobre o tratamento de exceções não poderia ser diferente, então proponho uma reflexão: Qual é o volume de exceções que a sua aplicação trata por dia/hora/minuto? Qual é o impacto disto na performance? O que acontece se esse volume aumentar?
O objetivo não é questionar a veracidade de uma exceção, pois em muitos casos elas são extremamente necessárias e eficientes, o ponto aqui é, demonstrar o custo computacional relacionado ao uso de uma estratégia tratamento de exceção (Exception-Handling), para que você as use conscientemente e saiba quando evitá-las.
O mecanismo de tratamento de exceção do .NET Framework foi construído sobre o mecanismo do Windows nomeado de Structured Exception Handling (SEH).
Observe o código a seguir:
try{
}
catch (NullReferenceException e)
{
}
catch (Exception e)
{
}
finally
{
}
As exceções são tratadas por uma instrução try.
Quando ocorre uma exceção, o sistema procura a cláusula catch mais próxima que possa tratar a exceção, conforme determinado pelo tipo da exceção.
Em um alto nível o que acontece em tempo de execução é o seguinte:
Sugiro a leitura da documentação oficial da Microsoft para entender com mais profundidade: Exceptions - C# language specification | Microsoft Docs
Até aqui entendemos muito no alto nível o que acontece, porém existem diversos outros detalhes tanto do CLR quanto do próprio SO que não foram discutidos.
Chris Brumme escreveu um excelente artigo chamado The Exception Model que detalha de forma cirúrgica todo o processo de exception-handling do CLR: The Exception Model - cbrumme's WebLog - Site Home - MSDN Blogs (archive.org)
Em resumo deste artigo podemos definir que, por baixo dos panos os seguintes fenômenos também acontecem:
Até aqui acredito que concordamos que, tratar exceções não é uma tarefa simples, tão pouco barata em termos computacionais, todavia, o uso destes mecanismos nos oferece uma robustez enorme.
O famoso dialeto “melhor prevenir do que remediar” vai exatamente de encontro com o que quero propor aqui.
Analise este método:
public int Dividir(int a, int b)
{
try
{
return a / b;
}
catch (DivideByZeroException ex)
{
return throw ex;
}
}
Meu ponto aqui é: Se eu sei que o usuário pode e vai colocar uma entrada inválida, o fato de eu estar ciente que esse cenário acontece com certa frequência, já me indica para evitar o lançamento desta Exception.
Após uma pequena refatoração chegaremos ao seguinte código:
public int Dividir(int a, int b)
{
try
{
if (a == 0 || b == 0) return 0;
return a / b;
}
catch
{
return 0;
}
}
Repare que não retirei o bloco try/catch pois neste ponto do código outros problemas dos quais ainda não tenho consciência podem ocorrer, entretanto agora eu definitivamente tenho a certeza de que não ocorrerá problemas com divisão por 0.
Vale a ressalva de que esse tipo de abordagem traz um efeito negativo para expressividade do seu código, esse tema foi bastante discutido no livro “Clean Code: A Handbook of Agile Software Craftsmanship” do Robert C. Martin.
O ideal aqui é colocar na balança: Performance vs Expressividade.
Para tomar essa decisão sempre precisamos pensar no volume de requisições que nossa aplicação normalmente/excepcionalmente vai lidar.
Um pouco de empirismo nunca é demais.
Publiquei um repositório no GitHub com uma API para que você consiga reproduzir o mesmo cenário que o meu: https://github.com/claudiogodoy99/Exceptions-Lab.git
Para isso você vai precisar ter:
Publiquei a API no recurso de PaaS do Azure chamado Azure Web App, isso ajudou muito a ter um ambiente completamente limpo para os testes.
O cenário de teste é bem simples, teremos dois endpoints com a mesma função:
app.MapGet("/dividir/handle", (int a, int b) => {
try{
int result = a / b;
return result;
}
catch (DivideByZeroException ex){
throw ex;
}
});
app.MapGet("/dividir", (int a, int b) => {
if (a == 0 || b == 0) return 0;
return a / b;
});
A sutil diferença entre eles é que um trata os valores de entrada afim de evitar um Exception de divisão por 0.
No primeiro caso o throw no bloco catch é proposital, toda API AspNet já tem um global exception handler default, que traduz qualquer exception para o Status Response 500, o que estamos fazendo aqui é deixar que esse global exception handler capture nossa exceção de divisão por 0.
Usaremos a ferramenta JMetter para simular um alto volume de requisições simultâneas e planilhar os resultados.
Meu ponto aqui não é te ensinar a configurar um teste de carga, para entender melhor o que vou falar a seguir sugiro uma breve lida na documentação oficial do JMtter.
Essa é a estrutura do nosso cenário de teste:
Basicamente vou reutilizar o mesmo teste para os dois endpoints, com uma carga idêntica (50% das requisições contêm valores inválidos), e comparar os resultados.
Assim poderemos claramente evidenciar todos os efeitos discutidos anteriormente, e ainda melhor, vamos ver isso ocorrendo com um alto volume de requisições simultâneas.
Os parâmetros do teste de carga são:
app.MapGet("/dividir/handle", (int a, int b) => {
try
{
int result = a / b;
return result;
}
catch (DivideByZeroException ex){
throw ex;
}
});
A carga foi completa em 2,36 minutos:
Label |
# Samples |
Average |
Min |
Max |
Throughput |
Request OK |
6000 |
312 |
151 |
2885 |
42,41872 |
Request With Exception |
6000 |
819 |
157 |
15990 |
42,61757 |
TOTAL |
12000 |
566 |
151 |
15990 |
84,71705 |
app.MapGet("/dividir", (int a, int b) =>{
if (a == 0 || b == 0) return 0;
return a / b;
});
A carga foi completa em 41 segundos:
Label |
# Samples |
Average |
Throughput |
Request OK |
6000 |
180 |
145,6735 |
Request With Exception |
6000 |
175 |
150,09005 |
TOTAL |
12000 |
178 |
289,9181 |
Vou destacar os seguintes pontos:
Até aqui acredito que ficou claro a diferença drástica entre os resultados, mas para todo efeito vou elencar todos os fatos até aqui:
O próximo passo agora é conseguir responder a seguinte pergunta: Como eu identifico que minha aplicação está gerando mais exception do que deveria? Como eu encontro a causa raiz dessas exceptions e penso em uma forma de mitigá-las?
Pois até aqui nós montamos um cenário completamente simples e controlado, na realidade as coisas são um pouco mais complexas e críticas.
Mas vou deixar essas perguntas em aberto para respondê-las em breve.
Espero ter contribuído de alguma forma para sua jornada! Até a próxima!
Exceptions - C# language specification | Microsoft Docs
The Exception Model - cbrumme's WebLog - Site Home - MSDN Blogs (archive.org)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.