Das Paket BenchmarkDotNet vereinfacht Performanceuntersuchungen von Implementationen. Die hier beschriebenen Messungen erfolgten auf einem i7-1065G7 (Mobile CPU) mit 16 GB RAM unter Windows 11 Professional.
Als Datenmodellgrundlage verwenden wir folgendes Modell:

Dieses Modell weist Probleme auf, mit denen wir uns in den nächsten Beitragen auseinandersetzen werden.
Als aktuelle Arbeitsgrundlage möchten wir uns folgendes Szenario vorstellen:
Wir verfügen über einen Fuhrpark verschiedener Fahrzeuge. Diese können entweder vom Typen Bicycle sein oder vom Typen Car sein. Jeder Typ Car verfügt über die Eigenschaft FuelType und FuelAmount.
Als erstes erzeugen wir eine Methode in einer Benchmark-Klasse, die unseren Fuhrpark anhand von Zufallsdaten mit definiertem Seed füllt. Der definierte Seed erlaubt es uns, für jeden Benchmark-Lauf die gleiche Datengrundlage zu gewährleisten.
Wir möchten auf Grundlage dieses Modells das Laufzeitverhalten verschiedener Methoden zur Ermittlung, wie Diesel sich in unserer gesamten Flotte befindet, gegenüberstellen.
/// <summary>Benchmarking class for investigating different typecasts.</summary>
public class CastingBenchmarks
{
/// <summary>Random with given seed to ensure reproducibility of benchmark tests.</summary>
private static readonly Random Rnd = new Random(12389525);
/// <summary>Defines the length of the fleet to be initially created.</summary>
private static readonly int FleetLength = 1_000_000;
/// <summary>The fleet to benchmark.</summary>
private readonly Vehicle[] fleet;
/// <summary>Initializes the fleet to benchmark.</summary>
public CastingBenchmarks()
{
this.fleet = new Vehicle[FleetLength];
for (int i = 0; i < FleetLength; i++)
{
switch (Rnd.Next(0, 1))
{
case 0:
this.fleet[i] = new Bicycle(Rnd.NextDouble() * 20_000);
break;
case 1:
this.fleet[i] = new Car(Rnd.NextDouble() * 400_000, (FuelType)Rnd.Next(1, (int)FuelType.BatteryStoredEnergy), Rnd.NextDouble() * 90);
break;
}
}
}
/// <summary>Benchmark for the Linq-OfType-Performance.</summary>
[Benchmark]
public double LinqOfTypePerformance()
{
return this.fleet.OfType<Car>().Where(c => c.FuelType == FuelType.Diesel).Sum(c => c.FuelAmount);
}
/// <summary>Benchmark for the Linq-Cast-Performance.</summary>
[Benchmark]
public double LinqCastPerformance()
{
return this.fleet.Where(v => v.VehicleType == VehicleType.Car).Cast<Car>().Where(c => c.FuelType == FuelType.Diesel).Sum(c => c.FuelAmount);
}
}
Der Haupteinstiegspunkt einer Konsolenanwendung, die den Test zur Ausführung bringt, sieht denkbar einfach aus:
using BenchmarkDotNet.Running;
using SeeInsights.Benchmark;
internal class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run(typeof(CastingBenchmarks));
}
}
Der BenchmarkRunner bringt Tests ohne weitere Konfiguration nur dann zur Ausführung, wenn im Release-Modus gebaut wurde und kein Debugger attached ist. Ansonsten erscheint eine Fehlermeldung, die auf die Ursache hinweist.
Die Linq-Implementation erzeugt folgendes Ergebnis:
// * Summary *
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Mean | Error | StdDev |
|-------------------- |---------:|----------:|----------:|
| LinqOfTypePerformance | 9.018 ms | 0.1795 ms | 0.3626 ms |
9 Millisekunden um den Tankinhalt aller Dieselfahrzeuge in einer Flotte von 1.000.000 Fahrzeugen zu ermitteln ist nicht schlecht. Aber: Geht das auch ohne Parallelisierung noch etwas schneller?
Dazu erweitern wir unsere abstrakte Basisklasse Vehicle um eine abstrakte Eigenschaft VehicleType, die wir in den Ableitungen erweitern:
/// <summary>Abstract base record for all vehicles.</summary>
public abstract class Vehicle
{
/// <summary>Gets the specific vehicle type.</summary>
public abstract VehicleType VehicleType { get; }
/// <summary>
/// Gets the current vehicles mileage in metres.
/// </summary>
public double Mileage { get; private set; }
/// <summary>
/// Creates a new instance of the <see cref="Vehicle"/> class.
/// </summary>
/// <param name="mileage">The vehicle's mileage.</param>
public Vehicle(double mileage)
{
this.Mileage = mileage;
}
/// <summary>
/// Moves the vehicle and adds the distance to the vehicles <see cref="Mileage"/>.
/// </summary>
/// <param name="distance">The distance to move the vehicle in metres, must be equal or greater 0.</param>
public void Move(double distance)
{
if (distance < 0)
{
throw new ArgumentOutOfRangeException(nameof(distance), distance, "Distance must be greater or equal to 0.");
}
this.Mileage += distance;
}
}
/// <summary>Record reflecting a car.</summary>
public class Car : Vehicle
{
/// <summary>
/// Creates a new instance of the <see cref="Car"/> class.
/// </summary>
/// <param name="mileage">The cars current mileage.</param>
/// <param name="fuelType">The cars fuel type used.</param>
/// <param name="fuelAmount">the cars current fuel amount in the tank.</param>
public Car(double mileage, FuelType fuelType, double fuelAmount) : base(mileage)
{
this.FuelType = fuelType;
this.FuelAmount = fuelAmount;
}
/// <summary>Gets the current cars fuel amount available in the cars tank.</summary>
public double FuelAmount { get; private set; }
/// <summary>Gets the cars fuel type used.</summary>
public FuelType FuelType { get; init; }
/// <summary>
/// Gets the cars fuel consumption in litres per kilometres.
/// </summary>
public double FuelConsumptionInLitresPerKilometres { get; init; }
/// <summary>
/// Gets the vehicle type <see cref="VehicleType.Car"/> for this vehicle.
/// </summary>
public override VehicleType VehicleType => VehicleType.Car;
}
/// <summary>Record reflecting a bicycle.</summary>
public class Bicycle : Vehicle
{
/// <summary>
/// Creates a new instance of the <see cref="Bicycle"/> class.
/// </summary>
/// <param name="mileage">The bicycles current mileage.</param>
public Bicycle(double mileage) : base(mileage)
{
}
/// <summary>
/// Gets the vehicle type <see cref="VehicleType.Bicycle"/> for this vehicle.
/// </summary>
public override VehicleType VehicleType => VehicleType.Bicycle;
}
Daraus ergibt sich der Vorteil, dass wir ohne Patternmatching feststellen können, mit welcher konkreten Klasse wir es zu tun haben, vorab filtern können und einen Typecast verwenden können, um die Tankfüllung unser Dieselfahrzeuge auszulesen:
/// <summary>Benchmark for the Linq-Cast-Performance.</summary>
[Benchmark]
public double LinqCastPerformance()
{
return this.fleet.Where(v => v.VehicleType == VehicleType.Car).Cast<Car>().Where(c => c.FuelType == FuelType.Diesel).Sum(c => c.FuelAmount);
}
Der Performanceimpakt dieser Änderung ist offensichtlich:
// * Summary *
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Mean | Error | StdDev |
|---------------------- |---------:|----------:|----------:|
| LinqOfTypePerformance | 8.784 ms | 0.2174 ms | 0.6131 ms |
| LinqCastPerformance | 1.924 ms | 0.0384 ms | 0.0730 ms |
Wir brauchen, um die gleiche Information zu ermitteln, auf einmal nur noch knapp über 20% der ursprünglichen Zeit. Es gibt massenweise, aus performancetechnischer Sicht noch schlechtere Implementationen. Dieses Problem über Reflection zu lösen, ist zwar möglich, lässt die Ausführungszeit jedoch explodieren.
Zusätzlich ließe sich diese Problemstellung auch noch sehr gut parallelisieren. Darauf werde ich später auch eingehen. Dies soll nur ein erster Einblick in die Möglichkeiten des Packages DotNetBenchmark sein.
Mit dem hier vorgestelltem Objektmodellentwurf steuern wir geradewegs auf eine Verletzung des Liskov’schen Substitutionsprinzip zu. Doch das ist ein Thema für einen zukünftigen Beitrag.
Das aktuelle Modell sieht so aus:

Schreibe einen Kommentar