Cargando...

Uso de BenchmarkDotNet en C#

Aprende BenchmarkDotNet en C# para medir rendimiento y optimizar tu código con métricas precisas.

La medición del rendimiento (benchmarking) consiste en comparar dos o más enfoques en términos de velocidad, consumo de memoria y escalabilidad. En el mundo de .NET, BenchmarkDotNet es el estándar de la industria para esta tarea. Optimiza la compilación, realiza un calentamiento (warmup), ejecuta múltiples iteraciones, produce análisis estadísticos (media, mediana, desviación estándar) y genera informes detallados. En este artículo, verás cómo instalar BenchmarkDotNet, usarlo en comparaciones básicas y aplicarlo en escenarios reales.


Instalación

Agrega el paquete BenchmarkDotNet a tu proyecto de prueba o benchmark:


dotnet new console -n EjemploBenchmark
cd EjemploBenchmark
dotnet add package BenchmarkDotNet

Se recomienda ejecutar el programa en modo Release; BenchmarkDotNet lo gestiona automáticamente, pero la configuración del proyecto debe estar actualizada.


Primer Benchmark: Comparación simple

El siguiente ejemplo compara dos métodos de concatenación de cadenas. Cada método de benchmark se marca con [Benchmark]; el ejecutor se llama mediante BenchmarkRunner.Run<T>().


using System;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class StringBenchmarks
{
    private const int N = 1000;

    [Benchmark]
    public string PlusOperator()
    {
        string s = string.Empty;
        for (int i = 0; i < N; i++)
            s += "x";
        return s;
    }

    [Benchmark]
    public string StringBuilderAppend()
    {
        var sb = new StringBuilder();
        for (int i = 0; i < N; i++)
            sb.Append('x');
        return sb.ToString();
    }
}

class Program
{
    static void Main() => BenchmarkRunner.Run<StringBenchmarks>();
}

Al ejecutarse, genera informes detallados (.md, .csv, .html) en la carpeta BenchmarkDotNet.Artifacts.


Atributos principales


using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class SearchBench
{
    private int[] _data;

    [Params(1_000, 100_000)]
    public int Size;

    [GlobalSetup]
    public void Setup()
    {
        var rnd = new Random(42);
        _data = new int[Size];
        for (int i = 0; i < Size; i++) _data[i] = rnd.Next();
        Array.Sort(_data);
    }

    [Benchmark(Baseline = true)]
    public bool LinearSearch() => Array.Exists(_data, x => x == 42);

    [Benchmark]
    public bool BinarySearch() => Array.BinarySearch(_data, 42) >= 0;
}

La opción Baseline = true permite calcular una proporción (Ratio) en la tabla de resultados para comparar rendimientos.


Trabajos, runtime y configuración del entorno

Con las configuraciones de Job se pueden definir el entorno de ejecución, el JIT, la plataforma y el modo del recolector de basura (GC).


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net472)]
[MemoryDiagnoser]
public class HashBench
{
    [Benchmark] public int DefaultHash() => "abcdef".GetHashCode();
}

class Program
{
    static void Main() => BenchmarkRunner.Run<HashBench>();
}

Esta configuración permite comparar el rendimiento en varios runtimes en una sola ejecución (por ejemplo, .NET 8.0 vs .NET Framework 4.7.2).


Warmup, iteraciones y manejo de valores atípicos

BenchmarkDotNet realiza automáticamente fases de calentamiento y múltiples iteraciones para reducir el ruido de medición. También puedes configurarlas manualmente:


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;

[Config(typeof(MyConfig))]
public class MathBench
{
    private class MyConfig : ManualConfig
    {
        public MyConfig()
        {
            AddColumn(BenchmarkDotNet.Columns.StatisticColumn.Mean,
                      BenchmarkDotNet.Columns.StatisticColumn.StdDev);
            AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default);
            AddJob(BenchmarkDotNet.Jobs.Job.Default
                .WithWarmupCount(3)
                .WithIterationCount(10));
        }
    }

    [Benchmark] public double Sqrt() => Math.Sqrt(12345.6789);
}

Las estadísticas incluyen Mean, Median, StdDev, Min y Max; los valores atípicos pueden filtrarse para obtener análisis más precisos.


Comparaciones Paramétricas

Con [Params], puedes comparar diferentes tamaños de datos o estrategias en una sola tabla.


using BenchmarkDotNet.Attributes;
using System.Linq;

[MemoryDiagnoser]
public class SumBench
{
    [Params(10, 1000, 100000)]
    public int N;

    private int[] _arr;

    [GlobalSetup]
    public void Setup() => _arr = Enumerable.Range(1, N).ToArray();

    [Benchmark(Baseline = true)]
    public long ForLoop()
    {
        long s = 0;
        for (int i = 0; i < _arr.Length; i++) s += _arr[i];
        return s;
    }

    [Benchmark]
    public long LinqSum() => _arr.Sum(x => (long)x);
}

Los resultados te permiten observar cómo cambia el rendimiento según el valor de N, ayudándote a distinguir entre microoptimizaciones y diferencias algorítmicas.


Diagnóstico Avanzado: Desensamblado y Contadores de Hardware


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;

[DisassemblyDiagnoser(printSource: true, maxDepth: 2)]
public class CryptoBench
{
    [Benchmark] public byte[] GuidNew() => Guid.NewGuid().ToByteArray();
}

Estos diagnósticos son muy útiles para detectar asignaciones innecesarias y diferencias a nivel de instrucción en las rutas críticas del código.


Exportar Resultados (Exporters)

Los resultados pueden exportarse en formato Markdown, CSV o HTML — ideal para informes en CI/CD.


using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;

public class CsvConfig : ManualConfig
{
    public CsvConfig()
    {
        AddExporter(CsvExporter.Default);
        AddLogger(ConsoleLogger.Default);
    }
}

Errores Comunes y Mejores Prácticas


Ejemplo del Mundo Real: Comparación de Serialización JSON

El siguiente ejemplo compara la serialización JSON usando diferentes bibliotecas (solo con fines ilustrativos) y mide la asignación de memoria. Puedes ver claramente qué biblioteca es más rápida y asigna menos memoria para tu modelo de datos.


using System;
using System.Collections.Generic;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
// Extra: Si usas Newtonsoft.Json, agrega el paquete y descomenta la siguiente línea
// using Newtonsoft.Json;

[MemoryDiagnoser]
public class JsonBench
{
    private List<Persona> _lista;

    [Params(100, 10_000)]
    public int Count;

    [GlobalSetup]
    public void Setup()
    {
        _lista = new List<Persona>(Count);
        for (int i = 0; i < Count; i++)
            _lista.Add(new Persona { Nombre = "Ada", Apellido = "Lovelace", Edad = 28, Activa = (i % 2) == 0 });
    }

    [Benchmark(Baseline = true)]
    public string SystemTextJson() => JsonSerializer.Serialize(_lista);

    // [Benchmark]
    // public string NewtonsoftJson() => JsonConvert.SerializeObject(_lista);
}

public class Persona
{
    public string Nombre { get; set; }
    public string Apellido { get; set; }
    public int Edad { get; set; }
    public bool Activa { get; set; }
}

class Program
{
    static void Main() => BenchmarkRunner.Run<JsonBench>();
}

El informe muestra métricas como Mean, Error, StdDev y Allocated para cada método. Un valor Allocated más bajo reduce la presión del GC y puede marcar una gran diferencia bajo cargas de trabajo pesadas.


Integración CI/CD y Reproducibilidad


TL;DR

  • BenchmarkDotNet es el estándar para mediciones de rendimiento .NET confiables y reproducibles.
  • [Benchmark], [Params] y [MemoryDiagnoser] son los atributos clave.
  • Ejecuta los benchmarks en modo Release/x64 con datos realistas y asegúrate de usar el resultado.
  • Usa Jobs para comparar múltiples runtimes o configuraciones del GC.
  • Exporta los reportes como CSV/MD/HTML para seguimiento en CI/CD y detecta regresiones usando benchmarks baseline.

Artículos relacionados