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

Beyond the Code: KI verändert die Entwicklerrolle – wer sich jetzt anpasst, bleibt vorne dabei
Generative KI-Tools steigern Effizienz und beschleunigen Workflows – doch sie stellen auch neue Anforderungen an Entwicklerteams. Gefragt sind strategisches Denken, Kreativität und die Bereitschaft, sich laufend weiterzuentwickeln.
5 Minuten
15. Okt 2025
Datenspeicherung lokal und in der Cloud - Mobile Apps entwickeln mit Delphi, Teil 4
Mobile Apps müssen die Daten, die beispielsweise durch Sensoren ermittelt werden, zwischen den Sitzungen speichern und ebenso an einen entfernten Server übertragen.
7 Minuten
Copilot Agent Mode – Implementieren eines eigenen MCP-Servers
Wie KI-gestützte Development-Workflows von mit .NET selbst entwickelten MCP-Servern profitieren.
8 Minuten
13. Okt 2025

Das könnte Dich auch interessieren

Erstellung von ZUGFeRD 2.3 mit .NET C# - Rechnungserstellung
ZUGFeRD 2.3 konforme Rechnungen mit TX Text Control .NET Server für ASP.NET erstellen.
3 Minuten
9. Jan 2025
Wexflow: .NET Open Source Workflow-Engine - CodeProject
Wexflow ist eine quelloffene und plattformübergreifende Workflow-Engine und Automatisierungsplattform, die darauf abzielt, wiederkehrende Aufgaben zu automatisieren.
2 Minuten
Wann wird die Unterstützung für das .NET Framework eingestellt? - .NET
Ein Blick auf die Lebenszyklus-Richtlinien von Microsoft zeigt, dass das .NET Framework weiterhin unterstützt wird, während sich die Community auf die neuesten Versionen konzentriert.
2 Minuten
28. Jan 2025
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige