Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

Cześć wszystkim! Ten artykuł jest zbiorem Najlepszych Praktyk, z których ja i moi współpracownicy korzystamy od dłuższego czasu podczas pracy nad różnymi projektami.

Informacje o maszynie, na której wykonano obliczenia:BenchmarkDotNet=v0.11.5, system operacyjny=Windows 10.0.18362
Procesor Intel Core i5-8250U 1.60 GHz (Kaby Lake R), 1 procesor, 8 rdzeni logicznych i 4 rdzenie fizyczne
Zestaw SDK platformy .NET Core=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64-bitowy RyuJIT
Rdzeń: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64-bitowy RyuJIT
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64-bitowy RyuJIT
Rdzeń: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64-bitowy RyuJIT

Zadanie=Rdzeń Czas wykonania=Rdzeń

ToList vs ToArray i Cycles


Planowałem przygotować te informacje wraz z wydaniem .NET Core 3.0, ale mnie ubiegli, nie chcę kraść komuś chwały i kopiować cudzych informacji, więc tylko zwrócę uwagę link do dobrego artykułu, w którym szczegółowo opisano porównanie.

W swoim imieniu chcę tylko przedstawić Wam moje pomiary i wyniki, dodałem do nich pętle odwrotne dla miłośników „stylu C++” pisania pętli.

Kod:

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;
        }
    }

Prędkości wydajności w .NET Core 2.2 i 3.0 są prawie identyczne. Oto, co udało mi się uzyskać w .NET Core 3.0:

Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

Możemy stwierdzić, że iteracyjne przetwarzanie kolekcji Array jest szybsze dzięki wewnętrznym optymalizacjom i jawnej alokacji rozmiaru kolekcji. Warto również pamiętać, że kolekcja List ma swoje zalety i należy zastosować odpowiednią kolekcję w zależności od wymaganych obliczeń. Nawet jeśli napiszesz logikę pracy z pętlami, nie zapominaj, że jest to zwykła pętla i ona również podlega ewentualnej optymalizacji pętli. Artykuł na habr ukazał się dość dawno temu: https://habr.com/ru/post/124910/. Jest to nadal aktualna i zalecana lektura.

Rzucać

Rok temu pracowałem w firmie nad starszym projektem i w tym projekcie normalne było sprawdzanie poprawności pola za pomocą konstrukcji typu try-catch-throw. Już wtedy zrozumiałem, że jest to niezdrowa logika biznesowa dla projektu, dlatego gdy tylko było to możliwe, starałem się nie stosować takiego projektu. Ale zastanówmy się, dlaczego podejście do obsługi błędów przy takiej konstrukcji jest złe. Napisałem mały kod, aby porównać oba podejścia i stworzyłem testy porównawcze dla każdej opcji.

Kod:

        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;
        }

Wyniki w .NET Core 3.0 i Core 2.2 mają podobny wynik (.NET Core 3.0):

Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

Próba catch utrudnia zrozumienie kodu i wydłuża czas wykonywania programu. Ale jeśli potrzebujesz tej konstrukcji, nie powinieneś wstawiać tych linii kodu, które nie powinny obsługiwać błędów - dzięki temu kod będzie łatwiejszy do zrozumienia. W rzeczywistości to nie tyle obsługa wyjątków ładuje system, ile raczej wyrzucanie samych błędów poprzez nową konstrukcję wyjątku.

Zgłaszanie wyjątków jest wolniejsze niż w przypadku niektórych klas, które zbierają błędy w wymaganym formacie. Jeśli przetwarzasz formularz lub jakieś dane i doskonale wiesz, jaki powinien być błąd, dlaczego go nie przetworzyć?

Nie powinieneś pisać nowej konstrukcji rzutu wyjątku(), jeśli ta sytuacja nie jest wyjątkowa. Obsługa i zgłaszanie wyjątku jest bardzo kosztowna!!!

ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant

W ciągu 5 lat pracy na platformie .NET natknąłem się na wiele projektów wykorzystujących dopasowywanie ciągów znaków. Widziałem także następujący obraz: istniało jedno rozwiązanie Enterprise z wieloma projektami, z których każdy wykonywał porównania ciągów inaczej. Ale czego użyć i jak to ujednolicić? W książce CLR via C# autorstwa Richtera przeczytałem informację, że metoda ToUpperInvariant() jest szybsza niż ToLowerInvariant().

Fragment książki:

Wydajność w platformie .NET Core

Oczywiście nie wierzyłem w to i postanowiłem przeprowadzić wtedy kilka testów na .NET Framework, a wynik mnie zszokował - wzrost wydajności o ponad 15%. Następnie następnego ranka po przybyciu do pracy pokazałem te pomiary moim przełożonym i dałem im dostęp do kodu źródłowego. Następnie 2 z 14 projektów zmieniono w celu uwzględnienia nowych pomiarów, a biorąc pod uwagę, że te dwa projekty istniały do ​​przetwarzania ogromnych tabel Excel, wynik był więcej niż znaczący dla produktu.

Przedstawiam Wam również pomiary dla różnych wersji .NET Core, tak aby każdy z Was mógł dokonać wyboru w stronę najbardziej optymalnego rozwiązania. A dodam tylko, że w firmie, w której pracuję, do porównywania ciągów używamy ToUpper().

Kod:

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();
        }

Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

W platformie .NET Core 3.0 wzrost dla każdej z tych metod wynosi ~x2 i równoważy implementacje między sobą.

Wydajność w platformie .NET Core

Wydajność w platformie .NET Core

Kompilacja poziomów

W moim ostatnim artykule pokrótce opisałem tę funkcjonalność, chciałbym sprostować i uzupełnić swoje słowa. Kompilacja wielopoziomowa przyspiesza czas uruchamiania rozwiązania, ale wiąże się to z poświęceniem faktu, że części kodu zostaną skompilowane w tle do bardziej zoptymalizowanej wersji, co może spowodować niewielki narzut. Wraz z pojawieniem się platformy NET Core 3.0 czas kompilacji projektów z włączoną kompilacją warstw skrócił się, a błędy związane z tą technologią zostały naprawione. Wcześniej ta technologia prowadziła do błędów w pierwszych żądaniach w ASP.NET Core i zawieszała się podczas pierwszej kompilacji w trybie kompilacji wielopoziomowej. Obecnie jest ona domyślnie włączona w platformie .NET Core 3.0, ale w razie potrzeby można ją wyłączyć. Jeśli jesteś na stanowisku lidera zespołu, seniora, średniego szczebla lub jesteś szefem działu, to musisz zrozumieć, że szybki rozwój projektu podnosi wartość zespołu, a ta technologia pozwoli Ci zaoszczędzić czas obu programistów i czas samego projektu.

Poziom .NET wyższy

Uaktualnij wersję .NET Framework / .NET Core. Często każda nowa wersja zapewnia dodatkowy wzrost wydajności i dodaje nowe funkcje.

Ale jakie dokładnie są korzyści? Przyjrzyjmy się niektórym z nich:

  • W platformie .NET Core 3.0 wprowadzono obrazy R2R, które skracają czas uruchamiania aplikacji .NET Core.
  • W wersji 2.2 pojawiła się Tier Compilation, dzięki której programiści spędzą mniej czasu na uruchamianiu projektu.
  • Wsparcie dla nowych standardów .NET.
  • Wsparcie dla nowej wersji języka programowania.
  • Optymalizacja, z każdą nową wersją poprawia się optymalizacja podstawowych bibliotek Collection/Struct/Stream/String/Regex i wiele więcej. Jeśli przeprowadzasz migrację z .NET Framework do .NET Core, od razu uzyskasz duży wzrost wydajności. Jako przykład załączam link do niektórych optymalizacji, które zostały dodane do .NET Core 3.0: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-core-3-0/

Wydajność w platformie .NET Core

wniosek

Pisząc kod warto zwrócić uwagę na różne aspekty swojego projektu i wykorzystać możliwości swojego języka programowania i platformy, aby osiągnąć jak najlepszy efekt. Będzie mi miło, jeśli podzielisz się swoją wiedzą związaną z optymalizacją w .NET.

Link do githuba

Źródło: www.habr.com

Dodaj komentarz