Die Sommerferien sind vorbei, Kinder in der Schule, es regnet. Urlaubsende liegt in der Luft und dementsprechend bietet es sich an, ein paar Aufräumarbeiten umzusetzen.
Wir werden uns in diesem Beitrag mit den vorangegangenen Themen und einigen Unschönheiten bei der Verwendung des BenchmarkDotNet-Packages beschäftigen.
Implementationen können, abhängig von der Zahl ihrer Elemente, unterschiedlich skalieren. Bisher haben wir die vorangegangenen Benchmarks mit einer festen Anzahl an Elementen durchgeführt. Anstatt diese Benchmarks mit 1.000.000 Elementen durchzuführen, würde ich die Tests gerne mit 1, 100, 10.000 und 1.000.000 Elementen durchführen.
Dazu bietet uns BenchmarkDotNet ein das Params-Attribut, mit dem wir eine Eigenschaft unserer Benchmarkklasse dekorieren können:
/// Defines the length of the fleet to be benchmarked.
[Params(1, 100, 10_000, 1_000_000)]
public int FleetLength {get;set;}
Die Initialisierung des Vehicle-Arrays werden wir nicht mehr im Konstruktor unserer Benchmark-Klasse implementieren, sondern verschieben diese in eine Methode void GlobalSetup(), die wir mit dem Attribut GlobalSetup dekorieren. Als Folge daraus darf das Feld fleet nicht mehr länger readonly sein. Um CS8618 zu lösen, da das Feld fleet nach Verlassen des Konstruktors null ist, markieren wir es als nullable:
/// <summary>The fleet to benchmark.</summary>
private Vehicle[]? fleet;
/// <summary>
/// Initializes the fleet array with the desired length in <see cref="FleetLength"/>.
/// </summary>
[GlobalSetup]
public void GlobalSetup()
{
this.fleet = new Vehicle[this.FleetLength];
for (int i = 0; i < this.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, Rnd.NextDouble() * 9 + 3);
break;
}
}
}
Die Performance-Methoden lösen daraufhin CS8604 aus, weil fleet null ist und wir ohne Prüfung zugreifen. Anstatt die Prüfungen einzubauen, bestätigen wir dem Compiler: „Ja, das weiß ich und das ist absichtlich so gemacht.“, z.B.:
/// <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);
}
Nach Ausführung der Benchmarks ist die bisherige Ergebnisliste ein wenig umfangreicher:
Method | FleetLength | Mean | Error | StdDev | Median |
---|---|---|---|---|---|
LinqCastPerformance | 1 | 63.40 ns | 1.282 ns | 1.372 ns | 63.62 ns |
LinqOfTypePerformance | 1 | 54.63 ns | 0.725 ns | 0.605 ns | 54.74 ns |
LinqParallelCastPerformance | 1 | 6,316.22 ns | 117.683 ns | 270.395 ns | 6,295.80 ns |
LinqCastPerformance | 100 | 179.85 ns | 3.380 ns | 3.161 ns | 179.89 ns |
LinqOfTypePerformance | 100 | 710.26 ns | 12.267 ns | 12.048 ns | 709.54 ns |
LinqParallelCastPerformance | 100 | 7,451.50 ns | 203.800 ns | 588.010 ns | 7,308.79 ns |
LinqCastPerformance | 10000 | 10,342.38 ns | 197.132 ns | 227.017 ns | 10,340.83 ns |
LinqOfTypePerformance | 10000 | 66,340.92 ns | 572.327 ns | 924.202 ns | 66,251.06 ns |
LinqParallelCastPerformance | 10000 | 20,203.00 ns | 438.432 ns | 1,278.927 ns | 19,763.81 ns |
LinqCastPerformance | 1000000 | 1,859,208.64 ns | 37,099.729 ns | 57,759.809 ns | 1,858,081.25 ns |
LinqOfTypePerformance | 1000000 | 6,955,090.14 ns | 133,348.680 ns | 111,352.187 ns | 6,961,996.88 ns |
LinqParallelCastPerformance | 1000000 | 1,101,156.93 ns | 21,913.127 ns | 25,235.189 ns | 1,098,288.57 ns |
Jeder Benchmark wurde insgesamt 4 mal ausgeführt. Je Benchmark wurde eine FleetLength von 1, 100, 10.000 und 1.000.000 Elementen verwendet. Hieran lässt sich die Art der Skalierung erkennen. Die parallelisierte Abarbeitung unter Zuhilfenahme der AsParallel()-Erweiterungsmethode ist, wie man erkennen kann, mit einem Overhead verbunden, der erst ab einer Mindestanzahl von Elementen und dem möglichen Grad der Parallelisierung sowie der inneren Logik sinnvoll wird.
Die Konsolenausgabe ist schön, oftmals erkennt man unter Zuhilfenahme von Visualisierungen und Diagrammen Trends wesentlich schneller und einfacher. Wir dekorieren unsere Benchmarkklasse mit einem weiteren BenchmarkDotNet-Attribut:
/// Benchmarking class for investigating different typecasts.
[CsvExporter]
Public class Castingbenchmarks
Nach Ausführung der Benchmarks liegen in einem Unterverzeichnis (BenchmarkDotNet.Artifacts\results) des Ausführungsverzeichnisses unserer Konsolen-Benchmarkanwendung Report-Dateien. Darunter die gewünschte CSV-Datei, die z.B. mit Excel aufbereitet werden kann.
Wer Interesse an Diagrammen hat, dem sei ein Blick auf den RPlotExporter empfohlen.
Als Ausblick auf eines der nächsten Themen, hier noch die Messergebnisse einer weiteren Implementation:
Method | FleetLength | Mean | Error | StdDev |
---|---|---|---|---|
SpanForIterationByReferencePerformance | 1 | 1.509 ns | 0.0493 ns | 0.0461 ns |
SpanForIterationByReferencePerformance | 100 | 57.262 ns | 1.1457 ns | 1.4490 ns |
SpanForIterationByReferencePerformance | 10000 | 6,329.213 ns | 88.1814 ns | 82.4850 ns |
SpanForIterationByReferencePerformance | 1000000 | 1,659,565.951 ns | 31,418.0244 ns | 33,616.9341 ns |
Dieser Ansatz lässt sich auch mit kluger, flexibler Parallelisierung kombinieren, wodurch wir in allen Bereichen signifikant schneller das Ergebnis ermitteln können, als das unsere bisherige Spitzenreiter-Implementation konnte.
Schreibe einen Kommentar