Wydajność w .NET Core

Cześć wszystkim! Artykuł ten jest zbiorem najlepszych praktyk, które ja i moi koledzy stosujemy od długiego czasu w różnych projektach.
Informacje o maszynie, na której przeprowadzono obliczenia:BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 rdzeni logicznych i 4 rdzenie fizyczne
.NET Core SDK=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
Core: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), 64bit RyuJIT
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
Core: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), 64bit RyuJIT
Job=Core Runtime=Core
ToList vs ToArray i Cycles
Te informacje planowałem przygotować z wydaniem .NET Core 3.0, ale mnie uprzedzili, nie chcę kradzież cudzej chwały i kopiować innych informacji, dlatego po prostu podam .
Od siebie tylko chcę przedstawić Wam moje pomiary i wyniki, dodałem w nich odwrotne cykle dla miłośników pisania cykli „w stylu C++”.
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;
}
}
Szybkość działania w .NET Core 2.2 i 3.0 jest prawie identyczna. Oto co udało mi się osiągnąć w .NET Core 3.0:


Możemy dojść do wniosku, że przetwarzanie kolekcji typu Array w pętli jest szybsze dzięki wewnętrznym optymalizacjom i wyraźnemu określeniu rozmiaru kolekcji. Należy również pamiętać, że kolekcja typu List ma swoje zalety i warto korzystać z odpowiedniej kolekcji w zależności od potrzebnych obliczeń. Nawet pisząc logikę pracy z pętlami, nie można zapominać, że to zwykła pętla, która także podlega potencjalnej optymalizacji. Na habr od dość dawna ukazał się artykuł: . Wciąż jest aktualny i warto go przeczytać.
Throw
Rok temu pracowałem w firmie nad projektem legacy, w którym normalną praktyką było przetwarzanie walidacji pól przy użyciu konstrukcji try-catch-throw. Już wtedy rozumiałem, że to niezdrowa logika biznesowa projektu, więc starałem się unikać takiej konstrukcji, gdy tylko było to możliwe. Ale przyjrzyjmy się, dlaczego podejście do obsługi błędów przy użyciu tej konstrukcji jest złe. Napisałem mały kod, aby porównać dwa podejścia i przeprowadziłem
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ą podobne rezultaty (.NET Core 3.0):


Try catch utrudnia zrozumienie kodu i zwiększa czas wykonywania programu. Ale jeśli potrzebujesz tej konstrukcji, nie wstawiaj tych linii kodu, od których nie oczekujesz obsługi błędów — to ułatwi zrozumienie kodu. W rzeczywistości to nie przetwarzanie wyjątków obciąża system, ale wyrzucanie samych błędów za pomocą konstrukcji throw new Exception.
Wyrzucanie wyjątków działa wolniej niż jakakolwiek klasa, która zbierze błąd w odpowiednim formacie. Jeśli przetwarzasz formularz lub jakieś dane i wyraźnie wiesz, jaki błąd powinien wystąpić, to czemu nie obsłużyć ich?
Nie należy używać konstrukcji throw new Exception(), jeśli dana sytuacja nie jest wyjątkowa. Obsługa i wywoływanie wyjątku są bardzo kosztowne!!!
ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant
W ciągu pięciu lat pracy z platformą .NET spotkałem wiele projektów, które wykorzystywały porównywanie ciągów. Widziałem także następujący obraz: jedno rozwiązanie klasy Enterprise z wieloma projektami, z których każdy realizował porównywanie ciągów w inny sposób. Ale co warto zastosować i jak to ujednolicić? W książce CLR via C# autorstwa Richtera przeczytałem, że metoda ToUpperInvariant() działa szybciej niż ToLowerInvariant().
Fragment z książki:

Oczywiście nie uwierzyłem i postanowiłem przeprowadzić kilka testów, jeszcze na .NET Framework, a wyniki mnie zszokowały — ponad 15% wzrost wydajności. Następnego ranka w pracy pokazałem wyniki moim przełożonym i udostępniłem im dostęp do źródeł. W rezultacie 2 z 14 projektów zostało dostosowanych do nowych pomiarów, a zważywszy na to, że te dwa projekty obsługiwały ogromne tabele Excel, wyniki były znaczące dla produktu.
Przedstawiam również pomiary dla różnych wersji .NET Core, aby każdy z Was mógł podjąć decyzję w kierunku najbardziej optymalnego rozwiązania. Chcę również dodać, że w firmie, w której pracuję, korzystamy z ToUpper() do porównywania ciągów.
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();
}


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


Kompilacja tierowa
W swoim poprzednim artykule opisałem tę funkcjonalność w skrócie, chciałbym poprawić i uzupełnić swoje słowa. Wiele poziomowa kompilacja przyspiesza czas uruchamiania Twojego rozwiązania, ale poświęcasz to, że części Twojego kodu będą kompilowane do bardziej zoptymalizowanej wersji w tle, co może prowadzić do niewielkich obciążeń. Od momentu pojawienia się .NET Core 3.0 czas kompilacji projektów z włączoną kompilacją poziomą zmniejszył się i naprawiono błędy związane z tą technologią. Wcześniej technologia ta prowadziła do błędów przy pierwszych żądaniach w ASP.NET Core i zawieszania podczas pierwszej kompilacji w trybie wielopoziomowej kompilacji. Na dzień dzisiejszy w .NET Core 3.0 jest ona włączona domyślnie, ale możesz ją wyłączyć na życzenie. Jeśli jesteś na stanowisku team-leadera, seniora, midera lub kierownika działu, powinieneś rozumieć, że szybkie tworzenie projektu zwiększa wartość zespołu, a ta technologia pozwoli Ci oszczędzać czas zarówno programistów, jak i czas pracy nad projektem.
.NET level up
Zaktualizuj wersję swojego .NET Framework / .NET Core. Często każda nowa wersja przynosi dodatkowy wzrost wydajności i dodaje nowe funkcje.
Ale jakie dokładnie korzyści? Spójrzmy na niektóre z nich:
- W .NET Core 3.0 pojawiły się obrazy R2R, które pozwolą zmniejszyć czas uruchamiania aplikacji .NET Core.
- Od wersji 2.2 pojawiła się kompilacja poziomowa, dzięki której programiści będą spędzać mniej czasu na uruchamianiu projektu.
- Wsparcie dla nowych standardów .NET Standard.
- Wsparcie dla nowej wersji języka programowania.
- Optymalizacja. Z każdą nową wersją poprawiana jest optymalizacja podstawowych bibliotek Collection/Struct/Stream/String/Regex i wiele innych. Jeśli przechodzisz z .NET Framework na .NET Core, otrzymasz duży wzrost wydajności z pudełka. Na przykład dołączam link do części optymalizacji, które zostały dodane w .NET Core 3.0:

Podsumowanie
Pisząc kod, warto zwrócić uwagę na różne aspekty Twojego projektu i wykorzystać funkcje swojego języka programowania oraz platformy, aby osiągnąć najlepsze wyniki. Będę wdzięczny, jeśli podzielisz się swoją wiedzą związaną z optymalizacją w .NET.
Źródło: habr.com
