Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 10 de marzo de 2020
.NET Core Alguna vez he escuchado, a modo de leyenda urbana, que no era bueno utilizar try/catch porque el mero hecho de usar este tipo de bloque de código afectaba al rendimiento.

La verdad es que de forma intuitiva se puede adivinar que esto no debería ser así. Si no se producen excepciones en un código que tengamos envuelto en un try/catch se debería ejecutar virtualmente a la misma velocidad que si no usáramos este tipo de bloque. Cosa distinta es si producen excepciones: ahí el rendimiento sí se verá seriamente penalizado, pero no es culpa del try/catch sino del propio sistema de gestión de excepciones de .NET.

Pero claro, lo dicho anteriormente es sólo cierto si somos capaces de demostrarlo, así que usaremos nuestro viejo conocido BenchmarkDotnet para desmontar este mito.

El método científico

Para demostrar nuestra afirmación, creamos una aplicación de consola .NET Core en la que añadimos la referencia al paquete NuGet BenchmarkDotNet e introducimos el siguiente código:
[MemoryDiagnoser]
public class Program
{
    static void Main(string[] args)
    {
        BenchmarkRunner.Run<Program>();
        Console.ReadLine();
    }

    [Benchmark]
    public int ConvertWithNoTryCatch()
    {
        return Convert.ToInt32("1");
    }

    [Benchmark]
    public int ConvertWithTryCatch()
    {
        try
        {
            return Convert.ToInt32("1");
        }
        catch (Exception)
        {
            return 0;
        }
    }
}
Como se puede ver en el código anterior, los métodos ConvertWithTryCatch() y ConvertWithoutTryCatch() realizan la simple conversión a enero de una cadena de caracteres (ya, sé que existe int.TryParse(), pero entonces no se generarían excepciones ;)). El resultado de ejecutar el test anterior es el siguiente:

|                Method |     Mean |    Error |   StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------- |---------:|---------:|---------:|------:|------:|------:|----------:|
| ConvertWithNoTryCatch | 11.33 ns | 0.029 ns | 0.027 ns |     - |     - |     - |         - |
|   ConvertWithTryCatch | 11.71 ns | 0.251 ns | 0.299 ns |     - |     - |     - |         - |
La pequeña diferencia, del orden de nanosegundos es puntual; si efectuamos la medición de nuevo, ambas implementaciones irán variando ligeramente sus tiempos, pero siempre alrededor de esos valores. Por tanto, atendiendo a estos datos podemos concluir que el uso de try/catch no afecta al rendimiento si no se producen excepciones.

¿Pero es esto una optimización de .NET Core, o es igual en .NET Framework? Pues para saberlo, ejecutamos la misma prueba con .NET Framework, y los resultados que obtenemos en esta ocasión son los siguientes:

|                Method |     Mean |    Error |   StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------- |---------:|---------:|---------:|------:|------:|------:|----------:|
| ConvertWithNoTryCatch | 52.78 ns | 0.416 ns | 0.368 ns |     - |     - |     - |         - |
|   ConvertWithTryCatch | 52.42 ns | 0.295 ns | 0.276 ns |     - |     - |     - |         - |
Aunque con una importante diferencia de velocidad entre .NET Core y .NET Framework a favor del primero, en ambos casos se puede observar que try/catch sigue sin afectar al rendimiento cuando todo va bien.

Por tanto, mito desmontado ;) Podemos utilizar tranquilamente try/catch si tenemos algo que aportar al tratamiento de una excepción sin que esto afecte al rendimiento.

Aquí podríamos terminar este post, pero seguro que nos quedaríamos con la siguiente pregunta en el tintero...

¿Y qué pasa si salta una excepción?

Ah, amigos, aquí es cuando el panorama cambia totalmente. Todos sabemos que el lanzamiento de una excepción y la parafernalia que ello conlleva supone un auténtico “hachazo” al rendimiento de nuestras aplicaciones, pero llevémoslo a números.

El siguiente test obtiene el tiempo de ejecución del mismo método cuando todo va bien (ConversionOk()) y cuando se producen excepciones (ConversionError()):
[Benchmark]
public int ConversionOk()
{
    return ConvertWithTryCatch("1");
}

[Benchmark]
public int ConversionError()
{
    return ConvertWithTryCatch("hola");
}

private int ConvertWithTryCatch(string value)
{
    try
    {
        return Convert.ToInt32(value);
    }
    catch (Exception)
    {
        return 0;
    }
}
Y ojo al resultado, que indica que cuando se produce la excepción, en un escenario simple como el que estamos planteando en la prueba, el tiempo de ejecución es mil veces superior:
|          Method |         Mean |     Error |    StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------- |-------------:|----------:|----------:|-------:|------:|------:|----------:|
|    ConversionOk |     13.42 ns |  0.076 ns |  0.071 ns |      - |     - |     - |         - |
| ConversionError | 13,895.49 ns | 43.435 ns | 40.629 ns | 0.0610 |     - |     - |     536 B |
Obviamente, esta es una de las razones por las que no es recomendable utilizar excepciones para gestionar el flujo normal de nuestras aplicaciones, y suele hacerse sólo para implementar el tratamiento de casos realmente excepcionales.

Publicado en Variable not found.

5 Comentarios:

Bitcubico Technology dijo...

Muchas gracias por compartir este contenido, ha sido de mucha utilidad

Reynier dijo...

Hola Jm,

Interesante comparacion, conocia ya del penalty de perfomance no obstante verlo en numeros simpre hace saltar las alarmas.

Para el caso, por ejemplo, de violacion de reglas de negocios (que no son un caso excepcional, sino un comportamiento esperado) pues siempre ha estado el dilema de lanzar excepciones vs retornar estructuras (encapsulando, resultados errores etc).

Esta segunda aunque tendria mejor performance pues siempre es criticada por la complejidad que agrega (todo el que llame a esa funcion tendria que chequear el resultado, inyectando multiple branch en todos los callers, dificultad de mantenimiento, reglas externas al negocio introducidas para estos chequeos). Por lo que el behavior para la violacion de reglas de negocio sigue siendo lanzar excepciones.

Que opinas al respecto.

José María Aguilar dijo...

Hola!

@Christian, el try/catch debes usarlo principalmente cuando tengas algo que aportar al tratamiento de la excepción. Si no vas a hacer nada con ella, no tiene sentido, y es mejor dejarla que "ascienda" hacia capas superiores que quizás sí saben cómo procesarla.

@Reynier: entiendo que es un tema algo controvertido y muy sujeto a opiniones personales. Pero creo que al final, como ocurre casi siempre, depende del escenario.

Como yo lo veo es que si vamos a un bar y pedimos una cerveza, podemos esperar (básicamente) dos resultados: o que nos la den, o que no nos la den porque se ha acabado. Lo excepcional es que justo en ese momento el barril de cerveza explote y todos saltemos por los aires. Para mi gusto, este último caso es una excepción :)

En un bar, la falta de stock de cervezas es algo imperdonable ;), pero previsible desde el punto de vista del negocio y que no debe tratarse de la misma forma que si ocurre la explosión. La complejidad que aporta comprobar un retorno no debería ser mucho mayor que montar una cláusula catch para cada uno de los casos, en cada uno de los callers de un método.

En cuanto al impacto en el rendimiento, entiendo que en determinados escenarios no sea crítico y pueda considerarse el uso de excepciones para modelar el workflow de la aplicación. Por ejemplo, en una aplicación web de alta concurrencia, lanzar demasiadas excepciones podría tumbar el servicio, pero sin embargo, una aplicación empresarial de con pocos usuarios podría permitírselo. Lo dicho, depende del escenario.

Saludos!!

Unknown dijo...

Hola, no hace mucho que estoy leyendo acerca de arquitectura limpia y como usarla e implementarla, pero veo que allí en la parte de dominio existen las excepciones, en ton es llego a la conclusión que si debemos usarlas o mejor dicho crearlas pero no tratarlas, o que opinas al respecto.

José María Aguilar dijo...

Hola!

Las excepciones deberían tratarse en el punto donde tengamos algo que aportar a dicho tratamiento... y si muchas veces en el dominio no hay nada que hacer con ellas. Por tanto, en general, suelo tender a dejar que "fluyan" hacia arriba (o abajo, depende dónde dibujes tu UI ;)).

El lanzarlas desde el dominio, por supuesto, siempre que sean situaciones excepcionales (es decir, no usarlas para flujos normales del sistema, como hablábamos un par de comentarios atrás sobre la cerveza).

Saludos!

Saludos!