Fallen wir diesmal direkt mit der sprichwörtlichen Tür ins Haus:
Method | Job | Runtime | FleetLength | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
ForEachIteration | .NET 6.0 | .NET 6.0 | 1 | 3.195 ns | 0.2099 ns | 0.6090 ns | 3.143 ns | – | – | – |
ForIIteration | .NET 6.0 | .NET 6.0 | 1 | 4.181 ns | 0.1815 ns | 0.5294 ns | 4.000 ns | – | – | – |
LinqOfTypePerformance | .NET 6.0 | .NET 6.0 | 1 | 115.638 ns | 7.8305 ns | 20.4910 ns | 108.013 ns | 0.0343 | – | 144 B |
LinqParallelCastPerformance | .NET 6.0 | .NET 6.0 | 1 | 20,626.291 ns | 410.4717 ns | 1,171.0990 ns | 20,427.220 ns | 1.9531 | – | 8176 B |
SpanForIterationByReferencePerformance | .NET 6.0 | .NET 6.0 | 1 | 4.016 ns | 0.1864 ns | 0.5377 ns | 3.930 ns | – | – | – |
SpanForIterationPerformance | .NET 6.0 | .NET 6.0 | 1 | 3.568 ns | 0.1310 ns | 0.3800 ns | 3.530 ns | – | – | – |
ForEachIteration | .NET 8.0 | .NET 8.0 | 1 | 1.925 ns | 0.2366 ns | 0.6975 ns | 1.761 ns | – | – | – |
ForIIteration | .NET 8.0 | .NET 8.0 | 1 | 2.621 ns | 0.1234 ns | 0.3618 ns | 2.563 ns | – | – | – |
LinqOfTypePerformance | .NET 8.0 | .NET 8.0 | 1 | 80.595 ns | 2.5101 ns | 7.2021 ns | 79.526 ns | 0.0343 | – | 144 B |
LinqParallelCastPerformance | .NET 8.0 | .NET 8.0 | 1 | 18,398.318 ns | 748.8382 ns | 2,207.9663 ns | 18,018.951 ns | 1.9531 | – | 8176 B |
SpanForIterationByReferencePerformance | .NET 8.0 | .NET 8.0 | 1 | 2.099 ns | 0.1014 ns | 0.2908 ns | 2.058 ns | – | – | – |
SpanForIterationPerformance | .NET 8.0 | .NET 8.0 | 1 | 2.410 ns | 0.1180 ns | 0.3422 ns | 2.352 ns | – | – | – |
ForEachIteration | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 3.533 ns | 0.1778 ns | 0.5187 ns | 3.372 ns | – | – | – |
ForIIteration | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 4.041 ns | 0.1394 ns | 0.3954 ns | 4.024 ns | – | – | – |
LinqOfTypePerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 202.122 ns | 6.3453 ns | 18.2058 ns | 199.411 ns | 0.0496 | – | 209 B |
LinqParallelCastPerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 41,402.425 ns | 2,959.3635 ns | 8,725.7502 ns | 39,456.732 ns | 1.8311 | 0.0610 | 7982 B |
SpanForIterationByReferencePerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 92.662 ns | 1.8500 ns | 3.5198 ns | 92.335 ns | – | – | – |
SpanForIterationPerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 1 | 88.218 ns | 2.2224 ns | 6.3406 ns | 87.391 ns | – | – | – |
ForEachIteration | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 3.732 ns | 0.1122 ns | 0.1843 ns | 3.734 ns | – | – | – |
ForIIteration | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 5.601 ns | 0.1501 ns | 0.3654 ns | 5.540 ns | – | – | – |
LinqOfTypePerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 210.521 ns | 6.5248 ns | 18.8255 ns | 208.679 ns | 0.0496 | – | 209 B |
LinqParallelCastPerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 36,338.355 ns | 1,003.3575 ns | 2,926.8409 ns | 36,125.793 ns | 1.8311 | 0.0610 | 7981 B |
SpanForIterationByReferencePerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 95.460 ns | 1.7305 ns | 2.7944 ns | 94.814 ns | – | – | – |
SpanForIterationPerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 1 | 81.856 ns | 2.5334 ns | 7.3899 ns | 81.153 ns | – | – | – |
ForEachIteration | .NET 6.0 | .NET 6.0 | 10000 | 22,952.337 ns | 771.7217 ns | 2,275.4388 ns | 22,959.010 ns | – | – | – |
ForIIteration | .NET 6.0 | .NET 6.0 | 10000 | 23,236.938 ns | 724.9941 ns | 2,020.9938 ns | 22,717.865 ns | – | – | – |
LinqOfTypePerformance | .NET 6.0 | .NET 6.0 | 10000 | 165,207.916 ns | 5,007.2815 ns | 14,366.8293 ns | 161,838.037 ns | – | – | 144 B |
LinqParallelCastPerformance | .NET 6.0 | .NET 6.0 | 10000 | 100,265.381 ns | 6,354.7593 ns | 18,737.1511 ns | 96,767.963 ns | 1.9531 | – | 8177 B |
SpanForIterationByReferencePerformance | .NET 6.0 | .NET 6.0 | 10000 | 20,204.932 ns | 391.1524 ns | 450.4517 ns | 20,309.161 ns | – | – | – |
SpanForIterationPerformance | .NET 6.0 | .NET 6.0 | 10000 | 23,980.477 ns | 954.0262 ns | 2,782.9392 ns | 23,515.498 ns | – | – | – |
ForEachIteration | .NET 8.0 | .NET 8.0 | 10000 | 8,067.743 ns | 213.9685 ns | 624.1562 ns | 7,999.893 ns | – | – | – |
ForIIteration | .NET 8.0 | .NET 8.0 | 10000 | 14,671.885 ns | 430.0111 ns | 1,261.1483 ns | 14,388.570 ns | – | – | – |
LinqOfTypePerformance | .NET 8.0 | .NET 8.0 | 10000 | 94,391.197 ns | 2,279.5301 ns | 6,613.3300 ns | 93,559.656 ns | – | – | 144 B |
LinqParallelCastPerformance | .NET 8.0 | .NET 8.0 | 10000 | 64,690.254 ns | 4,214.7212 ns | 12,427.2006 ns | 63,837.616 ns | 1.9531 | – | 8230 B |
SpanForIterationByReferencePerformance | .NET 8.0 | .NET 8.0 | 10000 | 8,115.275 ns | 261.1368 ns | 765.8691 ns | 8,089.366 ns | – | – | – |
SpanForIterationPerformance | .NET 8.0 | .NET 8.0 | 10000 | 10,242.061 ns | 654.4640 ns | 1,835.1872 ns | 9,846.310 ns | – | – | – |
ForEachIteration | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 24,160.238 ns | 1,666.6025 ns | 4,914.0151 ns | 21,258.899 ns | – | – | – |
ForIIteration | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 25,045.952 ns | 794.7605 ns | 2,330.8949 ns | 24,686.707 ns | – | – | – |
LinqOfTypePerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 287,201.387 ns | 5,468.8233 ns | 5,115.5407 ns | 286,597.852 ns | – | – | 209 B |
LinqParallelCastPerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 142,866.523 ns | 5,702.3113 ns | 16,633.9099 ns | 139,671.899 ns | 1.8311 | 0.1221 | 7987 B |
SpanForIterationByReferencePerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 47,288.875 ns | 3,210.0274 ns | 9,414.4548 ns | 47,085.809 ns | – | – | – |
SpanForIterationPerformance | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 37,838.959 ns | 2,461.2474 ns | 7,257.0436 ns | 37,084.753 ns | – | – | – |
ForEachIteration | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 30,065.280 ns | 1,056.1619 ns | 3,064.1171 ns | 29,276.416 ns | – | – | – |
ForIIteration | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 30,510.202 ns | 925.4133 ns | 2,655.1843 ns | 30,080.426 ns | – | – | – |
LinqOfTypePerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 350,806.732 ns | 12,405.6056 ns | 35,990.9112 ns | 341,720.215 ns | – | – | 211 B |
LinqParallelCastPerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 129,103.810 ns | 3,238.2464 ns | 9,548.0424 ns | 128,201.807 ns | 1.8311 | 0.1221 | 8011 B |
SpanForIterationByReferencePerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 21,630.709 ns | 624.8556 ns | 1,741.8476 ns | 21,344.678 ns | – | – | – |
SpanForIterationPerformance | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 30,743.895 ns | 704.0257 ns | 2,053.6760 ns | 30,662.811 ns | – | – | – |
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:
Method | Job | Runtime | FleetLength | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|---|---|---|
ForEachIteration | .NET 6.0 | .NET 6.0 | 10000 | 22,952.337 ns | 771.7217 ns | 2,275.4388 ns | 22,959.010 ns | – | – | – |
ForEachIteration | .NET 8.0 | .NET 8.0 | 10000 | 8,067.743 ns | 213.9685 ns | 624.1562 ns | 7,999.893 ns | – | – | – |
ForEachIteration | .NET Framework 4.6.2 | .NET Framework 4.6.2 | 10000 | 24,160.238 ns | 1,666.6025 ns | 4,914.0151 ns | 21,258.899 ns | – | – | – |
ForEachIteration | .NET Framework 4.7.2 | .NET Framework 4.7.2 | 10000 | 30,065.280 ns | 1,056.1619 ns | 3,064.1171 ns | 29,276.416 ns | – | – | – |
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