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
[Benchmark]: Marca el método que se va a medir.[GlobalSetup]/[GlobalCleanup]: Se ejecuta una vez antes/después de todas las mediciones (por ejemplo, para preparar datos).[IterationSetup]/[IterationCleanup]: Se ejecuta antes/después de cada iteración.[Params(...)]: Ejecuta el mismo benchmark con diferentes valores de parámetros.[MemoryDiagnoser]: Añade métricas de memoria (Bytes asignados, Gen0/1/2).
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
- DisassemblyDiagnoser: muestra el código ensamblador generado por el JIT (análisis avanzado).
- HardwareCounters: proporciona contadores de CPU (CacheMisses, BranchMispredictions, etc. — depende del soporte de la plataforma).
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
- No compares con Stopwatch: los microbenchmarks tienen mucho ruido; el JIT y el GC pueden distorsionar las mediciones.
- Usa Release + x64: los builds de Debug son engañosos, ya que las optimizaciones del JIT están desactivadas.
- Evita efectos secundarios: si el resultado del benchmark no se usa, el JIT puede eliminarlo. Devuelve el resultado o usa
Consumer. - El calentamiento es esencial: las primeras ejecuciones están afectadas por el JIT y la caché; BenchmarkDotNet lo maneja automáticamente.
- Usa conjuntos de datos realistas: los datos artificialmente pequeños pueden conducir a conclusiones erróneas.
- Usa un entorno estable: los procesos en segundo plano o el antivirus pueden generar ruido.
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
- Ejecuta los benchmarks como una etapa separada del pipeline y archiva los resultados como
.csvo.md. - Usa hardware o VM consistentes; configura el plan de energía de la CPU en “Alto rendimiento”.
- Marca los puntos de referencia base (baseline) para detectar regresiones de rendimiento en los PR comparativos.
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
Biblioteca TPL y programación paralela en C#
Aprende Task Parallel Library y programación paralela en C# con Task, Parallel y ejemplos prácticos.
Gestión de memoria y Garbage Collector en C#
Aprende gestión de memoria y garbage collector en C# para comprender el ciclo de vida de objetos y la limpieza de memoria.
Optimización del rendimiento con Span<T> y Memory<T> en C#
Aprende optimización de rendimiento en C# con Span<T> y Memory<T> para gestionar memoria de forma eficiente.