Rendimiento de una aplicación en C#

Primero de todo vamos a definir qué es el rendimiento de una aplicación. 

En nuestro escenario podemos definir el rendimiento o benchmark, como la velocidad a la que una parte de código, ya sea un algoritmo o varios métodos son ejecutados en nuestro sistema.

Para ello utilizaremos una librería de benchmarking que podemos encontrar en nuget llamada “BechmarkDotNet” 

benchmarkdotnet

Una vez tenemos nuestra librería ya podemos comprobar el rendimiento. Pero para ello debemos crear nuestro caso de uso.

 

Caso de uso

En este escenario, he creado un ejemplo muy sencillo. En el que tenemos una clase genérica -https://www.netmentor.es/Entrada/generics-csharp- que compara dos listas de elementos:

public bool EqualsGeneric(IList<long> x, IList<long> y)
{
    if (ReferenceEquals(x, y))
        return true;
    if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
        return false;
    if (x.Count != y.Count)
        return false;
    for (var i = 0; i < x.Count; i++)
    {
        if (x[i] == null)
        {
            if (y[i] != null)
                return false;
        }
        else if(!x[i].Equals(y[i]))
        {
            return false;
        }
    }
    return true;
}

En este caso en concreto, vamos a comparar dos listas de elementos long, por lo que podemos cambiar el código al siguiente:

public bool EqualsLong(IList<long> x, IList<long> y)
{
    if (ReferenceEquals(x, y))
        return true;
    if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
        return false;
    if (x.Count != y.Count)
        return false;
    for (var i = 0; i < x.Count; i++)
    {
        if (!x[i].Equals(y[i]))
        {
            return false;
        }
    }
    return true;
}

Como podemos observar hemos removido dos condicionales if. Lo que quiere decir, que cuando hacemos el comparador genérico estamos perdiendo un poco de rendimiento. Ya que long no puede ser null, por lo tanto ambas comprobaciones son innecesarias en el caso de los longs, pero necesarias para string.

 

Preparar el escenario

En este post vamos a ver cómo comprobar el rendimiento. para ello creamos una clase que contenga ambos métodos, el comparador genérico y el comparador normal - hemos modificado el genérico para que sea algo más entendible -.

public class Ejemplo
{
    public bool EqualsGeneric(IList<long> x, IList<long> y)
    {
        if (ReferenceEquals(x, y))
            return true;
        if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
            return false;
        if (x.Count != y.Count)
            return false;
        for (var i = 0; i < x.Count; i++)
        {
            if (x[i] == null)
            {
                if (y[i] != null)
                    return false;
            }
            else if(!x[i].Equals(y[i]))
            {
                return false;
            }
        }
        return true;
    }

    public bool EqualsLong(IList<long> x, IList<long> y)
    {
        if (ReferenceEquals(x, y))
            return true;
        if (ReferenceEquals(x, null) || ReferenceEquals(y, null))
            return false;
        if (x.Count != y.Count)
            return false;
        for (var i = 0; i < x.Count; i++)
        {
            if (!x[i].Equals(y[i]))
            {
                return false;
            }
        }
        return true;
    }
}

Para nuestro ejemplo vamos a crear dos listas de 1millon de elementos cada una, simplemente en la segunda lista cambiaremos el último elemento para que sea diferente, y así probamos el camino más largo de cada uno de los métodos. 

public IList<long> Items { get; set; }
public IList<long> ItemsList2 { get; set; }

public Ejemplo()
{
    Items = Enumerable.Range(1, 1000000).Select(a => Convert.ToInt64(a)).ToList();
    ItemsList2 = Enumerable.Range(1, 1000000-1).Select(a => Convert.ToInt64(a)).ToList();
    ItemsList2.Add(1);
}

Debemos instanciar ambas listas en el constructor, ya que para evitar cualquier retraso en la ejecucion del codigo, osea en el resultado para ver cual es mejor, desde la librería benchmark nos indican que no debemos mandar elementos dinámicos a las funciones. 

Finalmente debemos crear los dos métodos que usaremos para comparar ambas listas.

public void BenchmarkGenericEquals()
{
    _ = EqualsGeneric(Items, ItemsList2);
}

public void BenchmarkLongEquals()
{
    _ = EqualsLong(Items, ItemsList2);
}

 

Benchmarking

Ha llegado el momento de utilizar la librería que hemos instalado. 

para ello necesitamos indicar el atributo [Benchmark] en cada uno de los métodos que queremos testear.

using BenchmarkDotNet.Running;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Attributes;

[Benchmark]
public void BenchmarkGenericEquals()
{
    _ = EqualsGeneric(Items, ItemsList2);
}

[Benchmark]
public void BenchmarkLongEquals()
{
    _ = EqualsLong(Items, ItemsList2);
}

Finalmente debemos indicar que queremos realizar el bechmark como tal. Para ello en nuestra clase principal, en nuestro caso el método main de nuestra aplicación de consola debemos de instanciar el BenchmarkRunner de la siguiente manera: 

Summary summary = BenchmarkRunner.Run<Ejemplo>();

Para imprimir los resultados únicamente debemos mostrar por pantalla, únicamente debemos pasar el summary a un Console.WriteLine:

Console.WriteLine(summary);

Si ejecutamos el programa, el benchmark empezará a correr pero debemos tener en cuenta que nuestro benchmark va a ejecutar múltiples test, no solo uno, por lo que nuestras comprobaciones serán más certeras que si utilizamos StopWatch, que personalmente era el que utilizaba hasta ahora. 

Una vez termine, puede tardar un tiempo, dependiendo la complejidad del algoritmo, debemos centrarnos principalmente en la parte final: 

resultado benchmark

Como podemos observar nos indica el método, el tiempo medio de ejecución, el margen de error y la desviación, estos datos se pueden modificar como nos explican en la documentación.

Finalmente el resultado nos indica que el método genérico, como podemos observar es un poco más lento que el construido específicamente para long.

 

Conclusión

  • Si queremos comprobar rendimiento es recomendable utilizar esta librería de benchmark sobre un StopWatch manual.
  • Mejorar el rendimiento puede ser recomendable, pero no siempre obligatorio, ya que mejorar el rendimiento puede causar que el código sea muy difícil de leer.
  • Aún así, cuando dudamos entre dos procesos, es totalmente recomendable quedarnos con el que mejor rendimiento nos de.