Leistung in .NET Core

Leistung in .NET Core

Leistung in .NET Core

Hallo zusammen! Dieser Artikel ist eine Sammlung von Best Practices, die meine Kollegen und ich seit langem bei der Arbeit an verschiedenen Projekten verwenden.

Informationen zur Maschine, auf der die Berechnungen durchgeführt wurden:BenchmarkDotNet=v0.11.5, Betriebssystem=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60 GHz (Kaby Lake R), 1 CPU, 8 logische und 4 physische Kerne
.NET Core SDK=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
Kern: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64-Bit-RyuJIT
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
Kern: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64-Bit-RyuJIT

Job=Kern Laufzeit=Kern

ToList vs. ToArray und Cycles


Ich hatte geplant, diese Informationen mit der Veröffentlichung von .NET Core 3.0 vorzubereiten, aber sie sind mir zuvorgekommen. Ich möchte nicht den Ruhm eines anderen stehlen und die Informationen anderer Leute kopieren, also möchte ich nur darauf hinweisen Link zu einem guten Artikel, in dem der Vergleich ausführlich beschrieben wird.

In meinem eigenen Namen möchte ich Ihnen nur meine Messungen und Ergebnisse vorstellen; für Liebhaber des „C++-Stils“ des Schreibens von Schleifen habe ich ihnen Rückwärtsschleifen hinzugefügt.

Code:

public class Bench
    {
        private List<int> _list;
        private int[] _array;

        [Params(100000, 10000000)] public int N;

        [GlobalSetup]
        public void Setup()
        {
            const int MIN = 1;
            const int MAX = 10;
            Random random = new Random();
            _list = Enumerable.Repeat(0, N).Select(i => random.Next(MIN, MAX)).ToList();
            _array = _list.ToArray();
        }

        [Benchmark]
        public int ForList()
        {
            int total = 0;
            for (int i = 0; i < _list.Count; i++)
            {
                total += _list[i];
            }

            return total;
        }
        
        [Benchmark]
        public int ForListFromEnd()
        {
            int total = 0;t
            for (int i = _list.Count-1; i > 0; i--)
            {
                total += _list[i];
            }

            return total;
        }

        [Benchmark]
        public int ForeachList()
        {
            int total = 0;
            foreach (int i in _list)
            {
                total += i;
            }

            return total;
        }

        [Benchmark]
        public int ForeachArray()
        {
            int total = 0;
            foreach (int i in _array)
            {
                total += i;
            }

            return total;
        }

        [Benchmark]
        public int ForArray()
        {
            int total = 0;
            for (int i = 0; i < _array.Length; i++)
            {
                total += _array[i];
            }

            return total;
        }
        
        [Benchmark]
        public int ForArrayFromEnd()
        {
            int total = 0;
            for (int i = _array.Length-1; i > 0; i--)
            {
                total += _array[i];
            }

            return total;
        }
    }

Die Leistungsgeschwindigkeiten in .NET Core 2.2 und 3.0 sind nahezu identisch. Folgendes konnte ich in .NET Core 3.0 erreichen:

Leistung in .NET Core

Leistung in .NET Core

Wir können daraus schließen, dass die iterative Verarbeitung einer Array-Sammlung aufgrund ihrer internen Optimierungen und der expliziten Zuweisung der Sammlungsgröße schneller ist. Denken Sie auch daran, dass eine List-Sammlung ihre eigenen Vorteile hat und Sie je nach den erforderlichen Berechnungen die richtige Sammlung verwenden sollten. Auch wenn Sie Logik für die Arbeit mit Schleifen schreiben, vergessen Sie nicht, dass es sich um eine gewöhnliche Schleife handelt und diese auch einer möglichen Schleifenoptimierung unterliegt. Vor einiger Zeit wurde auf habr ein Artikel veröffentlicht: https://habr.com/ru/post/124910/. Es ist immer noch relevant und eine empfehlenswerte Lektüre.

Werfen

Vor einem Jahr arbeitete ich in einem Unternehmen an einem Legacy-Projekt. In diesem Projekt war es normal, die Feldvalidierung durch ein Try-Catch-Throw-Konstrukt zu verarbeiten. Damals war mir bereits klar, dass dies eine ungesunde Geschäftslogik für das Projekt war, also habe ich versucht, wenn möglich, ein solches Design nicht zu verwenden. Aber lassen Sie uns herausfinden, warum der Ansatz zur Fehlerbehandlung bei einer solchen Konstruktion schlecht ist. Ich habe einen kleinen Code geschrieben, um die beiden Ansätze zu vergleichen, und Benchmarks für jede Option erstellt.

Code:

        public bool ContainsHash()
        {
            bool result = false;
            foreach (var file in _files)
            {
                var extension = Path.GetExtension(file);
                if (_hash.Contains(extension))
                    result = true;
            }

            return result;
        }

        public bool ContainsHashTryCatch()
        {
            bool result = false;
            try
            {
                foreach (var file in _files)
                {
                    var extension = Path.GetExtension(file);
                    if (_hash.Contains(extension))
                        result = true;
                }
                
                if(!result) 
                    throw new Exception("false");
            }
            catch (Exception e)
            {
                result = false;
            }

            return result;
        }

Die Ergebnisse in .NET Core 3.0 und Core 2.2 haben ein ähnliches Ergebnis (.NET Core 3.0):

Leistung in .NET Core

Leistung in .NET Core

Try Catch macht den Code schwerer verständlich und erhöht die Ausführungszeit Ihres Programms. Wenn Sie diese Konstruktion benötigen, sollten Sie jedoch keine Codezeilen einfügen, von denen nicht erwartet wird, dass sie Fehler behandeln – dies erleichtert das Verständnis des Codes. Tatsächlich ist es nicht so sehr die Behandlung von Ausnahmen, die das System belastet, sondern vielmehr das Auslösen von Fehlern selbst durch das Konstrukt „throw new Exception“.

Das Auslösen von Ausnahmen ist langsamer als das Auslösen einiger Klassen, die den Fehler im erforderlichen Format erfassen. Wenn Sie ein Formular oder einige Daten verarbeiten und genau wissen, was der Fehler sein sollte, warum verarbeiten Sie ihn dann nicht?

Sie sollten kein throw new Exception()-Konstrukt schreiben, wenn diese Situation keine Ausnahmesituation darstellt. Die Handhabung und das Auslösen einer Ausnahme ist sehr teuer!!!

ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant

Im Laufe meiner 5-jährigen Erfahrung mit der .NET-Plattform bin ich auf viele Projekte gestoßen, die String-Matching verwendeten. Außerdem habe ich folgendes Bild gesehen: Es gab eine Enterprise-Lösung mit vielen Projekten, die String-Vergleiche jeweils unterschiedlich durchführten. Aber was sollte verwendet werden und wie kann es vereinheitlicht werden? Im Buch CLR via C# von Richter habe ich Informationen gelesen, dass die Methode ToUpperInvariant() schneller ist als ToLowerInvariant().

Auszug aus dem Buch:

Leistung in .NET Core

Natürlich habe ich es nicht geglaubt und beschlossen, dann einige Tests mit dem .NET Framework durchzuführen, und das Ergebnis hat mich schockiert – mehr als 15 % Leistungssteigerung. Als ich dann am nächsten Morgen bei der Arbeit ankam, zeigte ich diese Messungen meinen Vorgesetzten und gewährte ihnen Zugang zum Quellcode. Danach wurden 2 von 14 Projekten geändert, um die neuen Messungen zu berücksichtigen, und wenn man bedenkt, dass diese beiden Projekte zur Verarbeitung riesiger Excel-Tabellen dienten, war das Ergebnis für das Produkt mehr als signifikant.

Ich präsentiere Ihnen auch Messungen für verschiedene Versionen von .NET Core, damit jeder von Ihnen eine Auswahl für die optimale Lösung treffen kann. Und ich möchte nur hinzufügen, dass wir in der Firma, in der ich arbeite, ToUpper() zum Vergleichen von Zeichenfolgen verwenden.

Code:

public const string defaultString =  "VXTDuob5YhummuDq1PPXOHE4PbrRjYfBjcHdFs8UcKSAHOCGievbUItWhU3ovCmRALgdZUG1CB0sQ4iMj8Z1ZfkML2owvfkOKxBCoFUAN4VLd4I8ietmlsS5PtdQEn6zEgy1uCVZXiXuubd0xM5ONVZBqDu6nOVq1GQloEjeRN8jXrj0MVUexB9aIECs7caKGddpuut3";

        [Benchmark]
        public bool ToLower()
        {
            return defaultString.ToLower() == defaultString.ToLower();
        }

        [Benchmark]
        public bool ToLowerInvariant()
        {
            return defaultString.ToLowerInvariant() == defaultString.ToLowerInvariant();
        }

        [Benchmark]
        public bool ToUpper()
        {
            return defaultString.ToUpper() == defaultString.ToUpper();
        }

        [Benchmark]
        public bool ToUpperInvariant()
        {
            return defaultString.ToUpperInvariant() == defaultString.ToUpperInvariant();
        }

Leistung in .NET Core

Leistung in .NET Core

In .NET Core 3.0 beträgt die Erhöhung für jede dieser Methoden ~x2 und gleicht die Implementierungen untereinander aus.

Leistung in .NET Core

Leistung in .NET Core

Tier-Zusammenstellung

In meinem letzten Artikel habe ich diese Funktionalität kurz beschrieben, ich möchte meine Worte korrigieren und ergänzen. Die mehrstufige Kompilierung beschleunigt die Startzeit Ihrer Lösung, Sie müssen jedoch darauf verzichten, dass Teile Ihres Codes im Hintergrund in eine optimiertere Version kompiliert werden, was zu einem geringen Mehraufwand führen kann. Mit der Einführung von NET Core 3.0 hat sich die Erstellungszeit für Projekte mit aktivierter Ebenenkompilierung verkürzt und mit dieser Technologie verbundene Fehler wurden behoben. Bisher führte diese Technologie zu Fehlern bei den ersten Anfragen in ASP.NET Core und zum Einfrieren beim ersten Build im mehrstufigen Kompilierungsmodus. Es ist derzeit in .NET Core 3.0 standardmäßig aktiviert, Sie können es jedoch bei Bedarf deaktivieren. Wenn Sie die Position eines Teamleiters, eines Seniors oder eines mittleren Teams innehaben oder der Leiter einer Abteilung sind, müssen Sie verstehen, dass eine schnelle Projektentwicklung den Wert des Teams steigert und diese Technologie es Ihnen ermöglicht, Zeit für beide Entwickler zu sparen und die Zeit des Projekts selbst.

.NET-Level aufsteigen

Aktualisieren Sie Ihre .NET Framework-/.NET Core-Version. Oft bietet jede neue Version zusätzliche Leistungssteigerungen und fügt neue Funktionen hinzu.

Aber was genau sind die Vorteile? Schauen wir uns einige davon an:

  • Mit .NET Core 3.0 wurden R2R-Images eingeführt, die die Startzeit von .NET Core-Anwendungen verkürzen.
  • Mit Version 2.2 erschien die Tier Compilation, dank der Programmierer weniger Zeit mit dem Starten eines Projekts verbringen müssen.
  • Unterstützung für neue .NET-Standards.
  • Unterstützung für eine neue Version der Programmiersprache.
  • Optimierung, mit jeder neuen Version verbessert sich die Optimierung der Basisbibliotheken Collection/Struct/Stream/String/Regex und vieles mehr. Wenn Sie von .NET Framework auf .NET Core migrieren, erhalten Sie sofort eine enorme Leistungssteigerung. Als Beispiel füge ich einen Link zu einigen Optimierungen hinzu, die zu .NET Core 3.0 hinzugefügt wurden: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/

Leistung in .NET Core

Abschluss

Beim Schreiben von Code lohnt es sich, auf verschiedene Aspekte Ihres Projekts zu achten und die Funktionen Ihrer Programmiersprache und Plattform zu nutzen, um das beste Ergebnis zu erzielen. Ich würde mich freuen, wenn Sie Ihr Wissen rund um die Optimierung in .NET teilen.

Link zu Github

Source: habr.com

Kommentar hinzufügen