Benchmarking mit BenchmarkDotNet in C#
Lernen Sie Benchmarking in C# mit BenchmarkDotNet zur genauen Leistungsanalyse und Optimierung.
Leistungsbewertung (Benchmarking) ist der Vergleich von zwei oder mehr Ansätzen hinsichtlich Geschwindigkeit, Speicherverbrauch und Skalierbarkeit. In der .NET-Welt ist BenchmarkDotNet der Industriestandard für solche Messungen. Es optimiert die Kompilierung, führt ein Aufwärmen (warmup) durch, wiederholt mehrere Messungen, erstellt statistische Analysen (Mittelwert, Median, Abweichung) und generiert Berichte. In diesem Artikel sehen Sie die Installation, die grundlegende Verwendung und praxisnahe Tipps zu BenchmarkDotNet.
Installation
Fügen Sie das BenchmarkDotNet-Paket zu Ihrem Test- oder Benchmark-Projekt hinzu:
dotnet new console -n BenchmarkBeispiel
cd BenchmarkBeispiel
dotnet add package BenchmarkDotNet
Es wird empfohlen, das Programm im Release-Modus auszuführen. BenchmarkDotNet verwaltet dies automatisch, aber die Projektkonfiguration sollte aktuell sein.
Erster Benchmark: Einfacher Vergleich
Das folgende Beispiel vergleicht zwei Methoden zur Zeichenkettenverknüpfung.
Jede Benchmark-Methode wird mit [Benchmark] markiert; der Runner wird mit BenchmarkRunner.Run<T>() aufgerufen.
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>();
}
Bei der Ausführung erstellt das Tool detaillierte Berichte (.md, .csv, .html) im Ordner BenchmarkDotNet.Artifacts.
Wichtige Attribute
[Benchmark]: Markiert die zu messende Methode.[GlobalSetup]/[GlobalCleanup]: Wird einmal vor bzw. nach allen Benchmarks ausgeführt (z. B. zur Datenvorbereitung).[IterationSetup]/[IterationCleanup]: Wird vor bzw. nach jeder Iteration ausgeführt.[Params(...)]: Führt denselben Benchmark mit unterschiedlichen Parameterwerten aus.[MemoryDiagnoser]: Fügt Speichermetriken hinzu (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;
}
Die Option Baseline = true ermöglicht die Berechnung eines Vergleichsverhältnisses (Ratio) in der Ergebnis-Tabelle.
Jobs, Laufzeit und Umgebungs-Einstellungen
Mit Job-Konfigurationen können Sie Ziel-Laufzeitumgebung, JIT, Plattform oder GC-Modus festlegen.
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>();
}
Diese Konfiguration ermöglicht es, mehrere Laufzeiten gleichzeitig zu vergleichen (z. B. .NET 8.0 vs .NET Framework 4.7.2).
Warmup, Iteration und Ausreißer-Verwaltung
BenchmarkDotNet führt automatisch Aufwärmphasen und mehrere Iterationen durch, um Messrauschen zu minimieren. Sie können diese Werte aber auch manuell konfigurieren:
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);
}
Die Statistiken enthalten Werte wie Mean, Median, StdDev, Min und Max; Ausreißer können herausgefiltert werden.
Parametrische Vergleiche
Mit [Params] können Sie verschiedene Datengrößen oder Strategien in einer einzigen Tabelle vergleichen.
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);
}
Die Ergebnisse zeigen, wie sich die Leistung je nach N-Wert verändert – so lässt sich leicht erkennen, ob es sich um Mikrooptimierungen oder algorithmische Unterschiede handelt.
Erweiterte Diagnose: Disassembly und Hardwarezähler
- DisassemblyDiagnoser: Zeigt den vom JIT erzeugten Assembly-Code an (für fortgeschrittene Analysen).
- HardwareCounters: CPU-Zähler (CacheMisses, BranchMispredictions usw. – abhängig von der Plattformunterstützung).
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
[DisassemblyDiagnoser(printSource: true, maxDepth: 2)]
public class CryptoBench
{
[Benchmark] public byte[] GuidNew() => Guid.NewGuid().ToByteArray();
}
Diese Diagnosen sind sehr hilfreich, um unnötige Speicherzuweisungen und Unterschiede auf Befehlsebene in „heißen“ Codepfaden zu erkennen.
Ausgabe exportieren (Exporters)
Ergebnisse können als Markdown-, CSV- oder HTML-Dateien exportiert werden – ideal für CI/CD-Berichte.
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
public class CsvConfig : ManualConfig
{
public CsvConfig()
{
AddExporter(CsvExporter.Default);
AddLogger(ConsoleLogger.Default);
}
}
Häufige Fehler und Best Practices
- Vergleichen Sie nicht mit Stopwatch: Mikrobenchmarks enthalten viel Rauschen; JIT- und GC-Effekte verfälschen die Messung.
- Release + x64: Debug-Builds sind irreführend, da JIT-Optimierungen deaktiviert sind.
- Nebenwirkungen vermeiden: Wenn das Benchmark-Ergebnis nicht verwendet wird, kann JIT es entfernen. Geben Sie das Ergebnis zurück oder verwenden Sie
Consumer. - Warmup ist notwendig: Die ersten Durchläufe sind durch JIT und Cache beeinflusst; BenchmarkDotNet erledigt dies automatisch.
- Reale Datensätze: Künstlich kleine Datenmengen können zu falschen Schlussfolgerungen führen.
- Feste Umgebung: Hintergrundprozesse oder Antivirensoftware verursachen Rauschen.
Praxisbeispiel: JSON-Serialisierungsvergleich
Im folgenden Beispiel werden verschiedene JSON-Bibliotheken (zu Demonstrationszwecken) verglichen und der Speicherverbrauch gemessen. So können Sie klar erkennen, welche Bibliothek für Ihr Datenmodell schneller arbeitet und weniger Speicher zuweist.
using System;
using System.Collections.Generic;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
// Hinweis: Wenn Sie Newtonsoft.Json verwenden, fügen Sie das Paket hinzu und heben Sie die Auskommentierung auf
// using Newtonsoft.Json;
[MemoryDiagnoser]
public class JsonBench
{
private List<Person> _list;
[Params(100, 10_000)]
public int Count;
[GlobalSetup]
public void Setup()
{
_list = new List<Person>(Count);
for (int i = 0; i < Count; i++)
_list.Add(new Person { Vorname = "Ada", Nachname = "Lovelace", Alter = 28, Aktiv = (i % 2) == 0 });
}
[Benchmark(Baseline = true)]
public string SystemTextJson() => JsonSerializer.Serialize(_list);
// [Benchmark]
// public string NewtonsoftJson() => JsonConvert.SerializeObject(_list);
}
public class Person
{
public string Vorname { get; set; }
public string Nachname { get; set; }
public int Alter { get; set; }
public bool Aktiv { get; set; }
}
class Program
{
static void Main() => BenchmarkRunner.Run<JsonBench>();
}
Der Bericht zeigt Metriken wie Mean, Error, StdDev und Allocated für jede Methode. Ein niedrigerer Allocated-Wert reduziert den GC-Druck und hat bei hoher Last einen großen Einfluss.
CI/CD-Integration und Wiederholbarkeit
- Führen Sie Benchmarks als separaten Pipeline-Schritt aus; archivieren Sie die Ergebnisse als
.csvoder.md. - Verwenden Sie identische Hardware oder VMs; stellen Sie den CPU-Energiesparmodus auf „Höchstleistung“.
- Markieren Sie Basislinien (baseline), um Leistungsregressionen in Pull Requests zu erkennen.
TL;DR
- BenchmarkDotNet ist der Standard für zuverlässige und wiederholbare Leistungsanalysen in .NET.
[Benchmark],[Params]und[MemoryDiagnoser]sind die zentralen Attribute.- Verwenden Sie Release/x64 und realistische Daten; stellen Sie sicher, dass das Ergebnis genutzt wird.
- Mit Jobs können Sie mehrere Runtimes und GC-Einstellungen vergleichen.
- Exportieren Sie Berichte als CSV/MD/HTML für CI/CD und erkennen Sie Regressionen mit baseline-Benchmarks.
Ähnliche Artikel
Leistungsoptimierung mit Span<T> und Memory<T> in C#
Lernen Sie Performanceoptimierung in C# mit Span<T> und Memory<T> für effiziente Speicherverwaltung.
Speicherverwaltung und Garbage Collector in C#
Lernen Sie Speicherverwaltung und Garbage Collector in C#, um Objektlebenszyklen und Speicherbereinigung zu verstehen.
Task Parallel Library (TPL) und Parallelprogrammierung in C#
Lernen Sie Task Parallel Library und Parallelprogrammierung in C# mit Task, Parallel und praktischen Beispielen.