Performance: Konsolidierung

Fallen wir diesmal direkt mit der sprichwörtlichen Tür ins Haus:

MethodJobRuntimeFleetLengthMeanErrorStdDevMedianGen0Gen1Allocated
ForEachIteration.NET 6.0.NET 6.013.195 ns0.2099 ns0.6090 ns3.143 ns
ForIIteration.NET 6.0.NET 6.014.181 ns0.1815 ns0.5294 ns4.000 ns
LinqOfTypePerformance.NET 6.0.NET 6.01115.638 ns7.8305 ns20.4910 ns108.013 ns0.0343144 B
LinqParallelCastPerformance.NET 6.0.NET 6.0120,626.291 ns410.4717 ns1,171.0990 ns20,427.220 ns1.95318176 B
SpanForIterationByReferencePerformance.NET 6.0.NET 6.014.016 ns0.1864 ns0.5377 ns3.930 ns
SpanForIterationPerformance.NET 6.0.NET 6.013.568 ns0.1310 ns0.3800 ns3.530 ns
ForEachIteration.NET 8.0.NET 8.011.925 ns0.2366 ns0.6975 ns1.761 ns
ForIIteration.NET 8.0.NET 8.012.621 ns0.1234 ns0.3618 ns2.563 ns
LinqOfTypePerformance.NET 8.0.NET 8.0180.595 ns2.5101 ns7.2021 ns79.526 ns0.0343144 B
LinqParallelCastPerformance.NET 8.0.NET 8.0118,398.318 ns748.8382 ns2,207.9663 ns18,018.951 ns1.95318176 B
SpanForIterationByReferencePerformance.NET 8.0.NET 8.012.099 ns0.1014 ns0.2908 ns2.058 ns
SpanForIterationPerformance.NET 8.0.NET 8.012.410 ns0.1180 ns0.3422 ns2.352 ns
ForEachIteration.NET Framework 4.6.2.NET Framework 4.6.213.533 ns0.1778 ns0.5187 ns3.372 ns
ForIIteration.NET Framework 4.6.2.NET Framework 4.6.214.041 ns0.1394 ns0.3954 ns4.024 ns
LinqOfTypePerformance.NET Framework 4.6.2.NET Framework 4.6.21202.122 ns6.3453 ns18.2058 ns199.411 ns0.0496209 B
LinqParallelCastPerformance.NET Framework 4.6.2.NET Framework 4.6.2141,402.425 ns2,959.3635 ns8,725.7502 ns39,456.732 ns1.83110.06107982 B
SpanForIterationByReferencePerformance.NET Framework 4.6.2.NET Framework 4.6.2192.662 ns1.8500 ns3.5198 ns92.335 ns
SpanForIterationPerformance.NET Framework 4.6.2.NET Framework 4.6.2188.218 ns2.2224 ns6.3406 ns87.391 ns
ForEachIteration.NET Framework 4.7.2.NET Framework 4.7.213.732 ns0.1122 ns0.1843 ns3.734 ns
ForIIteration.NET Framework 4.7.2.NET Framework 4.7.215.601 ns0.1501 ns0.3654 ns5.540 ns
LinqOfTypePerformance.NET Framework 4.7.2.NET Framework 4.7.21210.521 ns6.5248 ns18.8255 ns208.679 ns0.0496209 B
LinqParallelCastPerformance.NET Framework 4.7.2.NET Framework 4.7.2136,338.355 ns1,003.3575 ns2,926.8409 ns36,125.793 ns1.83110.06107981 B
SpanForIterationByReferencePerformance.NET Framework 4.7.2.NET Framework 4.7.2195.460 ns1.7305 ns2.7944 ns94.814 ns
SpanForIterationPerformance.NET Framework 4.7.2.NET Framework 4.7.2181.856 ns2.5334 ns7.3899 ns81.153 ns
ForEachIteration.NET 6.0.NET 6.01000022,952.337 ns771.7217 ns2,275.4388 ns22,959.010 ns
ForIIteration.NET 6.0.NET 6.01000023,236.938 ns724.9941 ns2,020.9938 ns22,717.865 ns
LinqOfTypePerformance.NET 6.0.NET 6.010000165,207.916 ns5,007.2815 ns14,366.8293 ns161,838.037 ns144 B
LinqParallelCastPerformance.NET 6.0.NET 6.010000100,265.381 ns6,354.7593 ns18,737.1511 ns96,767.963 ns1.95318177 B
SpanForIterationByReferencePerformance.NET 6.0.NET 6.01000020,204.932 ns391.1524 ns450.4517 ns20,309.161 ns
SpanForIterationPerformance.NET 6.0.NET 6.01000023,980.477 ns954.0262 ns2,782.9392 ns23,515.498 ns
ForEachIteration.NET 8.0.NET 8.0100008,067.743 ns213.9685 ns624.1562 ns7,999.893 ns
ForIIteration.NET 8.0.NET 8.01000014,671.885 ns430.0111 ns1,261.1483 ns14,388.570 ns
LinqOfTypePerformance.NET 8.0.NET 8.01000094,391.197 ns2,279.5301 ns6,613.3300 ns93,559.656 ns144 B
LinqParallelCastPerformance.NET 8.0.NET 8.01000064,690.254 ns4,214.7212 ns12,427.2006 ns63,837.616 ns1.95318230 B
SpanForIterationByReferencePerformance.NET 8.0.NET 8.0100008,115.275 ns261.1368 ns765.8691 ns8,089.366 ns
SpanForIterationPerformance.NET 8.0.NET 8.01000010,242.061 ns654.4640 ns1,835.1872 ns9,846.310 ns
ForEachIteration.NET Framework 4.6.2.NET Framework 4.6.21000024,160.238 ns1,666.6025 ns4,914.0151 ns21,258.899 ns
ForIIteration.NET Framework 4.6.2.NET Framework 4.6.21000025,045.952 ns794.7605 ns2,330.8949 ns24,686.707 ns
LinqOfTypePerformance.NET Framework 4.6.2.NET Framework 4.6.210000287,201.387 ns5,468.8233 ns5,115.5407 ns286,597.852 ns209 B
LinqParallelCastPerformance.NET Framework 4.6.2.NET Framework 4.6.210000142,866.523 ns5,702.3113 ns16,633.9099 ns139,671.899 ns1.83110.12217987 B
SpanForIterationByReferencePerformance.NET Framework 4.6.2.NET Framework 4.6.21000047,288.875 ns3,210.0274 ns9,414.4548 ns47,085.809 ns
SpanForIterationPerformance.NET Framework 4.6.2.NET Framework 4.6.21000037,838.959 ns2,461.2474 ns7,257.0436 ns37,084.753 ns
ForEachIteration.NET Framework 4.7.2.NET Framework 4.7.21000030,065.280 ns1,056.1619 ns3,064.1171 ns29,276.416 ns
ForIIteration.NET Framework 4.7.2.NET Framework 4.7.21000030,510.202 ns925.4133 ns2,655.1843 ns30,080.426 ns
LinqOfTypePerformance.NET Framework 4.7.2.NET Framework 4.7.210000350,806.732 ns12,405.6056 ns35,990.9112 ns341,720.215 ns211 B
LinqParallelCastPerformance.NET Framework 4.7.2.NET Framework 4.7.210000129,103.810 ns3,238.2464 ns9,548.0424 ns128,201.807 ns1.83110.12218011 B
SpanForIterationByReferencePerformance.NET Framework 4.7.2.NET Framework 4.7.21000021,630.709 ns624.8556 ns1,741.8476 ns21,344.678 ns
SpanForIterationPerformance.NET Framework 4.7.2.NET Framework 4.7.21000030,743.895 ns704.0257 ns2,053.6760 ns30,662.811 ns
Komplette Performancemessung

Auffällig ist, in der obenstehenden Tabelle, die die Ergebnisse für 1 und 10.000 Iterationen zeigt, das einfache foreach-Schleifekonstrukt über das C#-Keyword in dem meisten Fällen die beste Performance liefert. Das widerspricht ein wenig den intuitiven Erwartungen, dass eine for-i-Schleife performanter sein müsste.

Hintergrund ist hier, dass, obwohl Microsoft AsSpan() ursprünglich in .NET Framework 4.6.2 nicht verfügbar war, diese Methoden als Erweiterungsmethoden dennoch Einzug in .NET Framework fanden und auch intern genutzt werden. Durch die Übersetzung von C#-Code in MSIL und danach erst durch JIT-Compiling in maschinenspezifischen Code, ergeben sich entsprechende Möglichkeiten, die auch hin und wieder genutzt werden.

Obwohl, bei näherer Betrachtung, die Implementation von AsSpan() dem liskovschen Substitutionsprinzip widerspricht, ist der Performancegewinn nicht von der Hand zu weisen.

Als letzter Vergleich dieser Serie noch die obenstehende Liste ausschließlich mit den foreach-Läufen für 10.000 Iterationen:

MethodJobRuntimeFleetLengthMeanErrorStdDevMedianGen0Gen1Allocated
ForEachIteration.NET 6.0.NET 6.01000022,952.337 ns771.7217 ns2,275.4388 ns22,959.010 ns
ForEachIteration.NET 8.0.NET 8.0100008,067.743 ns213.9685 ns624.1562 ns7,999.893 ns
ForEachIteration.NET Framework 4.6.2.NET Framework 4.6.21000024,160.238 ns1,666.6025 ns4,914.0151 ns21,258.899 ns
ForEachIteration.NET Framework 4.7.2.NET Framework 4.7.21000030,065.280 ns1,056.1619 ns3,064.1171 ns29,276.416 ns
for each-Performance

Auch wenn in einigen Sonderfällen, wie beispielsweise einer sehr hohen Anzahl von Elementen durch spezielle Zugriffe und / oder Parallelisierung weitere Performance herausgeholt werden kann, würde ich diese Ansatz nur dann empfehlen, wenn es wirklich notwendig wäre.

Somit komme ich zu folgendem Fazit aus dieser ersten Reihe:

Die Performance von .NET-Anwendungen ist von mehreren Aspekten abhängig. Optimierter, eigener Code ist hier ein Bestandteil. Optimierungen in der CLR durch Updates, aber auch die Entscheidung, eine bestehende .NET-Anwendung auf ein neueres Zielframework zu setzen, können stark spürbare Auswirkungen auf die Performance haben. Die meisten dieser Auswirkungen sind positiv, das ist ein Kernanspruch der Entwicklung neuer .NET-Versionen.

Ebenso können Optimierungen und Anpassungen im JIT-Compiler das Laufzeitverhalten einer Anwendung, ohne dass es eines Update der Anwendung selbst bedarf, Einfluss auf das Laufzeitverhalten haben.

.NET-Anwendungen sind keine statischen Konstrukte, sondern befinden sie sich, selbst lange nachdem der Ersteller eine Software released hat, in einem indirektem Zyklus permanenter Optimierung.

Diese Aspekte müssen während der Entwicklung gegenwärtig sein. Entwickler verwenden gerne für alles Lösungen, die es schon gibt. Nur leider werden oftmals die einfachsten, sprachgegebenen Ansätze ignoriert.

In einem Projekt wurde ich bereits mit drei unterschiedlichen Packages konfrontiert, die alle nichts anderes machen als das Publisher-Subscriber-Pattern zu implementieren.

Dabei werden nicht nur die Hürden für die Projektpflege, sondern auch für solche „Optmierungen unter der Motorhaube“ unnötig erhöht. Ich selbst verfolge lieber einen defensiven Ansatz und solange es nicht absolut notwendig ist, ein NuGet-Package zu nutzen oder eine Eigenimplementation kein besseres Ergebnis erzielen könnte, nehme ich davon Abstand, mir Abhängigkeiten in meine Projekte reinzuziehen. Manchmal sollte man sich mit Abstand fragen, ob man nur auf der Suche nach einem Problem für eine Lösung ist oder die potentielle Abhängigkeit das Leben und damit die Pflege eines Projektes wirklich vereinfachen kann.


Schreibe einen Kommentar

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