Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 7 Min.

Benchmark im Unit-Test-Stil

NuGet-Pakete können das Leben des .NET-Entwicklers sogar um neue Werkzeuge erweitern, die sich auf den Entwicklungsworkflow (und nicht den bearbeiteten Code) auswirken. Mit BenchmarkDotNet steht ein Musterbeispiel dieses Konzepts zur Verfügung, das die Überprüfung der Performance von Code aller Arten ermöglicht.
© EMGenie

Unabhängig von allen Problemen, die man sich mit Microbenchmarks einhandelt, sei BenchmarkDotNet schon aufgrund seiner Vorgehensweise positiv hervorgehoben: Benchmarks entstehen hier in an Unit-Tests erinnernden Methoden, die das Framework danach zur Ausführung bringt. Auf diese Weise lässt sich der „Performance-Zustand“ des Systems als Ganzes beobachten, außerdem sind niederschwellige Tests zum Vergleich von Implementierungen und Vorgehensweisen vereinfacht möglich.

Einrichtung des Benchmark-Systems

Um die Komplexität in Grenzen zu halten, funktioniert das Benchmark-System nur mit Applikationen, die in Form einer Konsolenanwendung vorliegen. In den folgenden Schritten wollen wir ein Programmbeispiel namens NMGBenchConsole1 verwenden. Erweitern Sie es in NuGet danach um das Paket BenchmarkDotNet.

Die eigentliche Ausführung des Runners erfolgt über Code, der im Rahmen der main()-Methode der Kommandozeilenapplikation zur Anwendung gelangt. Für unser erstes Beispiel sind folgende Anpassungen erforderlich:

 

using BenchmarkDotNet.Running;
var summary = BenchmarkRunner.Run<BenchmrkWorkerClass>();

 

Testlogik wird in Form dedizierter Klassen angeliefert. Für einen ersten Test wollen wir die Kommandozeile mit lästigem Fauchen traktieren, weshalb wir folgenden Benchmarkblock konstruieren:

 

 

internal class BenchmrkWorkerClass {
    [Benchmark]
    public void HissOnce() {
        Console.WriteLine("Fauch!");
    }

    [Benchmark]
    public void HissALot() {
        for(int i=0;i<1000;i++)
            Console.WriteLine("Fauch!");
    }

}

 

Jede Testmethode ist mit der Annotation Benchmark auszustatten – zur Laufzeit erfolgt eine Reflection, um die zu erledigenden Payloads zu finden. Ein hoffnungsfroher Klick auf das Startsymbol liefert dann das in Bild 1 gezeigte Ergebnis.

 

Der Runner zeigt sich weinerlich (Bild 1)

Der Runner zeigt sich weinerlich (Bild 1)

© Autor

Da das Benchmarken von mit dem Debugger verbundenem Code zu verschiedenen Problemen führt, blockiert das Benchmark-Framework in diesem Fall den Start des Runners. Zur Behebung muss in Visual Studio ein Release-Build ausgewählt werden, eventuelle Fehlermeldungen des Debuggers sind zu ignorieren. Wichtig ist außerdem, die Klasse mit den Benchmarks nach folgendem Schema zu exponieren:

 

public class BenchmrkWorkerClass

 

Sofern die Behebung der vom Runner monierten Passagen erfolgreich verlief, liefert der Benchmark das in Bild 2 gezeigte Ergebnis.

Nach dem Konzert folgt die Analyse (Bild 2)
Nach dem Konzert folgt die Analyse (Bild 2) © Autor

Lasset uns fauchen

In der Praxis dienen Benchmarks oft dem Vergleich unterschiedlicher Systeme oder Komponenten. BenchmarkDotNet bildet dies durch das Baseline-Attribut ab, das nach folgendem Schema auf einen Benchmark angewendet wird:

 

 

[Benchmark(Baseline = true)]
public void HissOnce() {
    Console.WriteLine("Fauch!");
}

 

Daraufhin wird die von anderen Benchmark-Runs verbrauchte Zeit in der Ergebnisanalyse fortan in Vergleich gesetzt.

Tausend Fauchvorgänge brauchen 948-mal so viel Zeit wie eine alleinstehende Wefzerei (Bild 3)

Tausend Fauchvorgänge brauchen 948-mal so viel Zeit wie eine alleinstehende Wefzerei (Bild 3)

© Autor

Wer in einem Testfall mehrere Baselines benötigt, darf die einzelnen Testfälle nach dem folgenden Schema gruppieren. Die in BenchmarkCategory übergebenen Strings legen fest, welche Baseline zur Bewertung der jeweils erhaltenen Ergebnisse heranzuziehen ist:

 

 

[BenchmarkCategory("Fast"), Benchmark(Baseline = true)]
public void Time50() => Thread.Sleep(50);
[BenchmarkCategory("Fast"), Benchmark]
public void Time100() => Thread.Sleep(100);

 

Das hier gezeigte Snippet würde zwei Gruppen anlegen. Die obigen Benchmarks beziehen sich auf Fast, während die folgenden zur Gruppe Slow gehören:

 

[BenchmarkCategory("Slow"), Benchmark(Baseline = true)]
public void Time550() => Thread.Sleep(550);
[BenchmarkCategory("Slow"), Benchmark]
public void Time600() => Thread.Sleep(600);

 

Interessant ist die Möglichkeit, die Verhaltensweisen der verschiedenen Benchmark-Routinen durch Parameter festzulegen. Zunächst wollen wir die Anzahl der vom Programm zu generierenden Fauchereien modifizieren. Um die Ausführungsdauer in Grenzen zu halten, treten wir anfangs jeweils zehn und 200 Wefzvorgänge los. Hierzu ist die Deklaration eines Params-Objekts erforderlich, das nach folgendem Schema aufzubauen ist:

 

public class BenchmrkWorkerClass {

    [Params(10, 200)]
    public int N { get; set; }

 

Die Liste bestimmt dann, welche Werte der Runner während der Ausführung des Benchmarks in das Feld N einzuschreiben hat.

Das eigentliche Musizieren nutzt nun den Params-Wert N als Obergrenze für die for-Schleife:

 

[Benchmark]
public void HissALot() {
    for(int i=0;i<N;i++)
        Console.WriteLine("Fauch!");
}

 

Daraus resultieren die in Bild 4 gezeigten Ergebnisse. Offensichtlich ist, dass sich der Wert von N nur auf den Testfall HissALot, aber nicht auf seinen Kollegen HissOnce auswirkt.

Der Wert von N ist nur für einen der beiden Benchmarks relevant (Bild 4)

Der Wert von N ist nur für einen der beiden Benchmarks relevant (Bild 4)

© Autor

Eine „Sondervariante“ von Params ist für die Verarbeitung von Enums vorgesehen. [ParamsAllValues] ermöglicht es nach folgendem Schema, alle Felder der Enum als im Rahmen des Benchmark-Runs zu testende Werte festzulegen:

 

public enum CustomEnum         {
    One = 1,
    Two,
    Three
}
[ParamsAllValues]
public CustomEnum E { get; set; }

 

Fauchen im Konzert

Obwohl Konsolenausgabeoperationen nicht zu den am schnellsten ablaufenden Aufgaben der Systemtechnik gehören, ist die für die Abarbeitung unserer Benchmark-Suite notwendige Zeit vergleichsweise lang. Als Erklärung hierfür könnte man annehmen, dass BenchmarkDotNet im Interesse der Herauskalibration von Ungenauigkeiten mehrere Durchläufe unseres Testfalls durchführt.

Zur Analyse dieses Verhaltens wollen wir die musikalische Testsuite auf die Methode HissOnce beschränken. In der Theorie dürfte die Ausführung des Programms dann nur noch zu einer einzigen Belästigung des Users führen.

Bei sorgfältiger Betrachtung der in der Konsole ausgegebenen Ergebnisse sehen wir einen nach dem Schema Mean = 137.139 us, StdErr = 0.712 us (0.52%), N = 23, StdDev = 3.417 us aufgebauten String; außerdem findet sich ein Histogramm mit verschiedenen Runtimes. Daraus lässt sich mit hoher Wahrscheinlichkeit schließen, dass der Runner von Haus aus mehrere Läufe absolviert.

Zur genaueren Untersuchung dieses Aspekts des Verhaltens sehen wir uns den Lebenszyklus unserer Testsuite näher an. Als Erstes betrachten wir die Attribute [GlobalSetup] und [GlobalCleanup], die – nomen est omen – den Beginn und das Ende der Ausführung der gesamten Testsuite anzeigen:

 

[GlobalSetup]
public void GlobalSetup()
{
    Console.WriteLine("Das Konzert beginnt!");
}

[GlobalCleanup]
public void GlobalCleanup()
{
    Console.WriteLine("Das Konzert endet!");
}

 

Auf Ebene der individuellen Testfälle lässt sich nach folgendem Schema deklarieren:

 

[IterationSetup]
public void IterationSetup()
    => Console.WriteLine("Die Wefze sagt: ");

[IterationCleanup]
public void IterationCleanup()
    => Console.WriteLine("und gibt wieder Ruhe!");

 

Zu guter Letzt sei noch angemerkt, dass Methoden wie GlobalSetup nur für einzelne Testfälle als relevant markiert werden können – dies setzt nach dem Schema [GlobalSetup(Targets = new[] { nameof(BenchmarkB), nameof(BenchmarkC) })]  aufgebauten Code voraus. Dann ist eine SimpleJob-Deklaration erforderlich, die nach folgendem Schema die individuell notwendige Durchlaufzahl festlegt:

 

[SimpleJob(RunStrategy.Monitoring, launchCount: 1,
warmupCount: 0, iterationCount: 3)]
public class BenchmrkWorkerClass { ...

 

Wichtig ist, dass interpretative Runtimes – .NET gehört explizit dazu – seit langer Zeit verschiedene Pre-Kompilations-Features mitbringen. Um genauere Ergebnisse zu erhalten, kann es vernünftig sein, den Microbenchmark-Code mehrfach auszuführen.

Die Ergebnisse präsentieren sich nun wie in Bild 5 gezeigt.

Die Musik ertönt nun nur noch drei Mal (Bild 5)

Die Musik ertönt nun nur noch drei Mal (Bild 5)

© Autor

Schon aus Platzgründen kann dieser Artikel nicht auf alle Konfigurationsmöglichkeiten eingehen, die der Runner zur Verfügung stellt. In der Dokumentation von BenchmarkDotNet finden sich detaillierte weitere Informationen.

 

Fauchen auf Schallplatten!

Sinn der Ausführung von Benchmarks ist in vielen Fällen die Generierung eines Paper Trails. Obwohl die hier abgedruckten Screenshots der Runtime durchaus ansprechend aussehen, ist es in der Praxis wünschenswert, dass der Runner persistierbare Resultate erzeugt. Dies ist die Aufgabe der Exporters-Module, die in der BenchmarkDotNet-Dokumentation en détail dokumentiert sind.

Wir wollen an dieser Stelle nur einen grundlegenden HTML-Exporter einsetzen, was folgende Modifikation an der Deklaration unseres Jobs voraussetzt:

 

namespace NMGBenchConsole1
{
    [SimpleJob(RunStrategy.Monitoring, launchCount: 1,
    warmupCount: 0, iterationCount: 3)]
    [HtmlExporter]
    public class BenchmrkWorkerClass

 

Die Programmausführung des Benchmark-Harnischs erfolgt dann ohne wesentliche Besonderheiten. Bei sorgfältiger Betrachtung der Ausgabe lässt sich allerdings wie in Bild 6 gezeigt feststellen, dass der Runner über die erfolgreiche Ausgabe informiert.

Der Export der Daten verlief erfolgreich (Bild 6)

Der Export der Daten verlief erfolgreich (Bild 6)

© Autor

Für die Aberntung der Ergebnisse bietet es sich an, das Projekt im Project Explorer rechts anzuklicken und die Option Enthaltenden Ordner öffnen zu wählen. Auf der Workstation des Autors findet sich im Ordner C:\Users\tamha\source\repos\NMGBenchConsole1\NMGBenchConsole1\bin\Release\net8.0\BenchmarkDotNet.Artifacts\results dann eine HTML-Datei, die sich im geöffneten Zustand wie in Bild 7 gezeigt präsentiert.

Die „Basisversion“ zeigt sich eher spröde (Bild 7)
Die „Basisversion“ zeigt sich eher spröde (Bild 7) © Autor

Fazit

Mit BenchmarkDotNet steht eine Infrastrukturkomponente zur Verfügung, die die Ermittlung von Performance-Informationen über .NET-Code erleichtert. Angesichts seiner immensen Flexibilität kommt das System nicht nur im Stand-alone-Betrieb, sondern auch als Komponente von anderen Utilities zum Einsatz.

Neueste Beiträge

MessagePack: JSON als Binär-Image - Best of NuGet, Teil 10
Das Serialisierungsformat MessagePack wurde – anders als JSON – konsequent auf die Bedürfnisse von Maschinen und damit auch auf höchstmögliche Kompaktheit optimiert. Der Vorteil: Ein geringeres zu übertragendes Datenvolumen führt vor allem in IoT-nahen Anwendungen direkt zu geringeren Kosten.
6 Minuten
17. Aug 2026
Die Kunst der langen Aufgabenläufe - Kimi K3 – das Modell hinter dem Agenten, Teil 2
Ein guter Code-Generator löst eine klar umrissene Funktion. Ein Coding-Agent muss dagegen ein Repository erkunden, Hypothesen bilden, Werkzeuge auswählen, Tests auswerten und nach Fehlschlägen den Kurs ändern. Kimi K3 zielt auf diese zweite Klasse von Aufgaben.
8 Minuten
Gebaut für lange Läufe - Kimi K3 – das Modell hinter dem Agenten, Teil 1
Kimi K3 kombiniert 2,8 Billionen Parameter, native Bildverarbeitung und ein Kontextfenster mit einer Million Token. Das sind tolle Zahlen. Seine Besonderheit liegt aber darin, dass Architektur, Training und Laufzeitumgebung auf Agenten zielen, die über viele Schritte handeln, Werkzeuge aufrufen und Ergebnisse prüfen.
6 Minuten

Das könnte Dich auch interessieren

Das Firmata-Protokoll für einfache Mess- und Steuerungsaufgaben - Best of NuGet, Teil 8
Wer kleinere Mess-, Steuerungs- und Regeltechnik-Aufgaben aus .NET heraus abwickeln möchte, kommt mit einem preiswerten und mit Firmata-Software ausgestatteten Arduino meist problemlos ans Ziel.
7 Minuten
27. Jul 2026
.NET-Anbindung für die Llama-KI - LLamaSharp
Open-Source-KI auf dem lokalen Gerät nutzen – mit handhabbarem Speicherbedarf und optionalem Zugriff auf externe Datenquellen.
8 Minuten
13. Jan 2025
.NET-Color-Picker für WPF- und Avalonia-Applikationen - Best of NuGet, Teil 7
Mit PixiEditor.ColorPicker steht eine Bibliothek in den Startlöchern, die Entwicklern von Desktop-zentrierten Applikationen verschiedene hochwertige Farbauswahldialoge zur Verfügung stellt.
6 Minuten
20. Jul 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige