Housekeeping

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:

MethodFleetLengthMeanErrorStdDevMedian
LinqCastPerformance163.40 ns1.282 ns1.372 ns63.62 ns
LinqOfTypePerformance154.63 ns0.725 ns0.605 ns54.74 ns
LinqParallelCastPerformance16,316.22 ns117.683 ns270.395 ns6,295.80 ns
LinqCastPerformance100179.85 ns3.380 ns3.161 ns179.89 ns
LinqOfTypePerformance100710.26 ns12.267 ns12.048 ns709.54 ns
LinqParallelCastPerformance1007,451.50 ns203.800 ns588.010 ns7,308.79 ns
LinqCastPerformance1000010,342.38 ns197.132 ns227.017 ns10,340.83 ns
LinqOfTypePerformance1000066,340.92 ns572.327 ns924.202 ns66,251.06 ns
LinqParallelCastPerformance1000020,203.00 ns438.432 ns1,278.927 ns19,763.81 ns
LinqCastPerformance10000001,859,208.64 ns37,099.729 ns57,759.809 ns1,858,081.25 ns
LinqOfTypePerformance10000006,955,090.14 ns133,348.680 ns111,352.187 ns6,961,996.88 ns
LinqParallelCastPerformance10000001,101,156.93 ns21,913.127 ns25,235.189 ns1,098,288.57 ns
Benchmark-Ergebnisse

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:

MethodFleetLengthMeanErrorStdDev
SpanForIterationByReferencePerformance11.509 ns0.0493 ns0.0461 ns
SpanForIterationByReferencePerformance10057.262 ns1.1457 ns1.4490 ns
SpanForIterationByReferencePerformance100006,329.213 ns88.1814 ns82.4850 ns
SpanForIterationByReferencePerformance10000001,659,565.951 ns31,418.0244 ns33,616.9341 ns
Span-Benchmarks

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

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert