Utilisation de BenchmarkDotNet en C#
Apprenez BenchmarkDotNet en C# pour mesurer les performances et optimiser votre code avec précision.
L’évaluation des performances (benchmarking) consiste à comparer deux ou plusieurs approches en termes de vitesse, de consommation mémoire et de scalabilité. Dans l’univers .NET, BenchmarkDotNet est la norme industrielle pour cette tâche. Il optimise la compilation, effectue un échauffement (warmup), exécute plusieurs itérations, produit des analyses statistiques (moyenne, médiane, écart-type) et génère des rapports détaillés. Cet article présente l’installation, l’utilisation de base et les bonnes pratiques de BenchmarkDotNet dans des scénarios concrets.
Installation
Ajoutez le package BenchmarkDotNet à votre projet de test ou de benchmark :
dotnet new console -n ExempleBenchmark
cd ExempleBenchmark
dotnet add package BenchmarkDotNet
Il est recommandé d’exécuter le programme en mode Release ; BenchmarkDotNet le gère automatiquement, mais votre configuration de projet doit être à jour.
Premier Benchmark : Comparaison simple
L’exemple suivant compare deux méthodes de concaténation de chaînes.
Chaque méthode de benchmark est marquée avec [Benchmark] ; le lanceur s’exécute via 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>();
}
Lors de l’exécution, il génère des rapports détaillés (.md, .csv, .html) dans le dossier BenchmarkDotNet.Artifacts.
Attributs principaux
[Benchmark]: Marque la méthode à mesurer.[GlobalSetup]/[GlobalCleanup]: S’exécute une fois avant/après toutes les mesures (préparation de données, par ex.).[IterationSetup]/[IterationCleanup]: S’exécute avant/après chaque itération.[Params(...)]: Exécute le même benchmark avec différentes valeurs de paramètres.[MemoryDiagnoser]: Ajoute des métriques mémoire (Allocated B, 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;
}
L’option Baseline = true permet de calculer un ratio de comparaison dans le tableau des résultats.
Jobs, runtime et configuration d’environnement
Les configurations de Job permettent de définir la version du runtime, le JIT, la plateforme, ou le mode du 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>();
}
Cette configuration permet de comparer les performances sur plusieurs runtimes en une seule exécution (par ex. .NET 8.0 vs .NET Framework 4.7.2).
Warmup, itérations et gestion des valeurs aberrantes
BenchmarkDotNet effectue automatiquement des phases d’échauffement et plusieurs itérations de mesure pour réduire le bruit. Vous pouvez également les configurer manuellement :
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);
}
Les statistiques incluent les valeurs Mean, Median, StdDev, Min et Max ; les valeurs aberrantes peuvent être filtrées pour une analyse plus précise.
Comparaisons Paramétriques
Avec [Params], vous pouvez comparer différentes tailles de données ou stratégies dans un seul tableau.
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);
}
Les résultats permettent d’observer comment la performance varie selon la valeur de N — vous pouvez ainsi distinguer une micro-optimisation d’une différence algorithmique.
Diagnostic Avancé : Désassemblage et Compteurs Matériels
- DisassemblyDiagnoser : affiche le code assembleur généré par le JIT (analyse avancée).
- HardwareCounters : fournit des compteurs CPU (CacheMisses, BranchMispredictions, etc. — selon la plateforme).
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
[DisassemblyDiagnoser(printSource: true, maxDepth: 2)]
public class CryptoBench
{
[Benchmark] public byte[] GuidNew() => Guid.NewGuid().ToByteArray();
}
Ces diagnostics sont très utiles pour repérer les allocations inutiles et les différences au niveau des instructions dans les chemins critiques du code.
Exporter les Résultats (Exporters)
Les résultats peuvent être exportés en Markdown, CSV ou HTML — idéal pour les rapports CI/CD.
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
public class CsvConfig : ManualConfig
{
public CsvConfig()
{
AddExporter(CsvExporter.Default);
AddLogger(ConsoleLogger.Default);
}
}
Erreurs Courantes et Bonnes Pratiques
- Ne pas comparer avec Stopwatch : les micro-benchmarks sont bruyants ; le JIT et le GC faussent les mesures.
- Release + x64 : les builds Debug sont trompeurs car les optimisations JIT sont désactivées.
- Évitez les effets de bord : si le résultat du benchmark n’est pas utilisé, le JIT peut l’éliminer. Retournez la valeur ou utilisez
Consumer. - Le warmup est essentiel : les premières exécutions sont affectées par le JIT et le cache ; BenchmarkDotNet gère cela automatiquement.
- Utilisez des ensembles de données réalistes : des données artificiellement petites peuvent conduire à de mauvaises conclusions.
- Utilisez un environnement stable : les tâches d’arrière-plan ou les antivirus peuvent introduire du bruit.
Exemple Réel : Comparaison de Sérialisation JSON
L’exemple ci-dessous compare la sérialisation JSON à l’aide de différentes bibliothèques (à titre illustratif) et mesure les allocations mémoire. Vous pouvez clairement voir quelle bibliothèque est la plus rapide et la plus économe pour votre modèle de données.
using System;
using System.Collections.Generic;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
// Supplément : si vous utilisez Newtonsoft.Json, ajoutez le package et décommentez la ligne ci-dessous
// using Newtonsoft.Json;
[MemoryDiagnoser]
public class JsonBench
{
private List<Personne> _liste;
[Params(100, 10_000)]
public int Count;
[GlobalSetup]
public void Setup()
{
_liste = new List<Personne>(Count);
for (int i = 0; i < Count; i++)
_liste.Add(new Personne { Prenom = "Ada", Nom = "Lovelace", Age = 28, Actif = (i % 2) == 0 });
}
[Benchmark(Baseline = true)]
public string SystemTextJson() => JsonSerializer.Serialize(_liste);
// [Benchmark]
// public string NewtonsoftJson() => JsonConvert.SerializeObject(_liste);
}
public class Personne
{
public string Prenom { get; set; }
public string Nom { get; set; }
public int Age { get; set; }
public bool Actif { get; set; }
}
class Program
{
static void Main() => BenchmarkRunner.Run<JsonBench>();
}
Le rapport affiche les métriques Mean, Error, StdDev et Allocated pour chaque méthode. Une valeur Allocated plus faible réduit la pression sur le GC et améliore les performances sous forte charge.
Intégration CI/CD et Reproductibilité
- Exécutez les benchmarks comme une étape séparée du pipeline et archivez les résultats au format
.csvou.md. - Utilisez un matériel ou une machine virtuelle identique ; définissez le mode d’alimentation du CPU sur « Haute performance ».
- Utilisez des repères de base (baseline) dans les PR comparatives pour détecter les régressions de performance.
TL;DR
- BenchmarkDotNet est la norme pour des mesures de performance .NET fiables et reproductibles.
[Benchmark],[Params]et[MemoryDiagnoser]sont les attributs clés.- Effectuez les benchmarks en mode Release/x64 avec des données réalistes, et assurez-vous que le résultat est utilisé.
- Utilisez Jobs pour comparer plusieurs runtimes ou configurations du GC.
- Exportez les rapports au format CSV/MD/HTML pour le suivi CI/CD et détectez les régressions à l’aide des benchmarks baseline.
Articles connexes
Bibliothèque parallèle TPL et programmation parallèle en C#
Apprenez la Task Parallel Library et la programmation parallèle en C# avec Task, Parallel et des exemples pratiques.
Gestion de la mémoire et Garbage Collector en C#
Apprenez la gestion de la mémoire et le garbage collector en C# pour comprendre le cycle de vie des objets.
Optimisation des performances avec Span<T> et Memory<T> en C#
Apprenez l’optimisation des performances en C# avec Span<T> et Memory<T> pour une gestion mémoire efficace.