Umarł .NET Framework, niech żyje .NET Core (oraz .NET 5) i jego wydajność

Czas się pożegnać 

Pierwsze wydanie .NET Frameworka 1.0 to początek roku 2002. W tym momencie najnowsza wersja wersja 4.8 będzie ostatnim głównym wydaniem klasycznego frameworku .NET. Oczywiście nadal będzie on dostarczany z Windowsem, ale to nie ma co się oszukiwać jest to już koniec. Microsoft nie będzie już dodawał nowych ficzerów czy optymalizacji do .NET Frameworku. Pozostanie czysty support i nic więcej, zatem możliwe będą aktualizacje bezpieczeństwa i zapewne nic więcej.

.NET Framework to także biblioteki i składowe, które zostaną pożegnane razem z odchodzącą wersją. Zatem nie usłyszymy już, albo będziemy słyszeć już coraz mniej, o WCF (trochę szkoda, aczkolwiek warto nadmienić, że zostaje wersja kliencka, ale nie serwerowa) czy WebFormsach (nareszcie! składam wyrazy współczucia każdemu, kto jeszcze w tej technologii pisze).

.NET Core (3.x) i .NET 5 - olbrzymi skok wydajnościowy

Wydany we wrześniu .NET Core 3.0 to nie tylko przeniesienie kolejnych rzeczy z .NET Frameworka do Corea. To również, a może i przede wszystkim, bardzo duży skok wydajnościowy. 

.NET Core 3.0 to nowe algorytmy do sortowania czy operacji na kolekcjach. To również nowe typy struktur: Span<T> oraz Memory<T> to dzięki nim operacje na stringach czy plikach XML lub JSON są 2x lub 3x szybsze w porównaniu do .NET Frameworka. Różnica jest bardzo duża.

Pozbyto się również starego kodu i zaktualizowano wiele bibliotek, ale także dodano kilka sztuczek, które podniosły wydajność w wielu newralgicznych miejscach. Autoryzacja online to olbrzymi ilość szyfrowania, haszowania itd. Z tego też powodu w .NET Core operacje kryptograficzne wykonywanie są w... natywnym kodzie. Tak nie jest uruchamiany kod napisany w .NET, ale brane są natywne biblioteki napisane w C++. Tak oto pod Windowsem ujrzymy CNG Windows, zaś na Linuxie .NET Core do kryptografii zaprzęgnie OpenSSL.

Co więcej, dopiero .NET Core wykorzystuje sprzętowe możliwości Ryzenów w kwestii obliczeń kryptograficznych. Więcej w zewnętrznym wpisie: Will AMD’s Ryzen finally bring SHA extensions to Intel’s CPUs? Intel zostaje daleeeeko w tyle.

Nie tak dawno Microsoft wydał już 3. wersję preview kolejnego środowiska runtime - .NET 5. Nie jest to rewolucja, jaką był .NET Core w porównaniu do .NET Frameworka, ale kolejny etap w ewolucji .NET. Stawia on na usprawnienia .NET Core i ujednolicenie środowisk uruchomieniowych.

Jeśli chodzi o wydajność .NET 5, to jest zbliżona do wydajności .NET Core 3.x (na ten moment). W wersji preview 3 wydajność została "jedynie" bardzo podkręcona w temacie wyrażeń regularnych. Tutaj obliczenia mogą być nawet 60824x szybsze!

Nie chcąc być gołosłownym przedstawię porównanie działania tego samego kodu na .NET Frameworku i .NET Core. Do tego celu użyte były dwa procesory: Intel Core i7-4702MQ oraz AMD Ryzen R7 3700x (w porównaniu wydajności przy obliczeniu SHA256). Wyniki zmierzone przy użyciu BenchmarkDotNet (określają średni czas), kod bazuje na przykładach z blogu MS.

(Z racji tego, że BenchmarkDotNet opisuje .NET 5 jako .NET Core 5.0, tak też jest on oznaczony na wykresach i szczegółowych czasach)

Parsowanie Enuma:

public DayOfWeek EnumParse() => (DayOfWeek)Enum.Parse(typeof(DayOfWeek), "Thursday");

EnumParse .NET 4.8 199.1 ns

EnumParse .NET Core 3.1 124.4 ns

EnumParse .NET Core 5.0 125.7 ns

Operacje Linq:

//IEnumerable<int> _tenMillionToZero = Enumerable.Range(0, 10_000_000).Reverse();

public int LinqOrderBySkipFirst() => _tenMillionToZero.OrderBy(i => i).Skip(4).First();

LinqOrderBySkipFirst .NET 4.8 1,818,312,740.0 ns

LinqOrderBySkipFirst .NET Core 3.1 228,086,635.6 ns

LinqOrderBySkipFirst .NET Core 5.0 222,083,673.8 ns

Obliczanie SHA256:

//SHA256 _sha256 = SHA256.Create();

public byte[] Sha256() => _sha256.ComputeHash(_raw);

Intel:

Sha256 .NET 4.8 1,028,103,786.7 ns

Sha256 .NET Core 3.1 500,472,964.3 ns

Sha256 .NET Core 5.0 504,334,426.7 ns

AMD:

Sha256 .NET 4.8 649,423,446.2 ns

Sha256 .NET Core 3.1 50,023,144.0 ns

Sha256 .NET Core 5.0 49,985,630.0 ns

Intel:

AMD:

Obliczenia na stringu:

//static string _s = "abcdefghijklmnopqrstuvwxyz";

public bool StringStartsWith()
{
    var data = false;
    for (int i = 0; i < 100_000_000; i++)
    {
        data = _s.StartsWith("abcdefghijklmnopqrstuvwxy-", StringComparison.Ordinal);
    }
    return data;
}

StringStartsWith .NET 4.8 1,918,666,640.0 ns

StringStartsWith .NET Core 3.1 880,179,750.0 ns

StringStartsWith .NET Core 5.0 858,676,357.1 ns

Deserializacja:

//byte[] _raw = new byte[100 * 1024 * 1024];

public object Deserialize()
{
    var books = new List<Book>();
    for (int i = 0; i < 1_00000; i++)
    {
        string id = i.ToString();
        books.Add(new Book { Name = id, Id = id });
    }

    var formatter = new BinaryFormatter();
    var mem = new MemoryStream();
    formatter.Serialize(mem, books);
    mem.Position = 0;

    return formatter.Deserialize(mem);
}

Deserialize .NET 4.8 980,524,740.0 ns

Deserialize .NET Core 3.1 421,885,039.1 ns

Deserialize .NET Core 5.0 415,699,686.8 ns

Regex:

Email:

_regexEmail = new Regex(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", RegexOptions.Compiled);

 _regexEmail.IsMatch(_commonInput);

Regex_Email .NET 4.8 2,299,723.7 ns

Regex_Email .NET Core 3.1 1,845,451.4 ns

Regex_Email .NET Core 5.0 961,795.4 ns

StrongPassword:

_regexStrongPassword = new Regex(@"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$", RegexOptions.Compiled);

_regexStrongPassword.IsMatch(_commonInput);

Regex_StrongPassword .NET 4.8 1,887.3 ns

Regex_StrongPassword .NET Core 3.1 1,726.2 ns

Regex_StrongPassword .NET Core 5.0 427.4 ns

SpanSearching:

_regexSpanSearching = new Regex("([ab]cd|ef[g-i])jklm", RegexOptions.Compiled);

_regexSpanSearching.IsMatch(_commonInput);

Regex_SpanSearching .NET 4.8 339,303.0 ns

Regex_SpanSearching .NET Core 3.1 295,767.1 ns

Regex_SpanSearching .NET Core 5.0 22,660.0 ns

BackTracking:

_regexBackTracking = new Regex("a*a*a*a*a*a*a*b", RegexOptions.Compiled);;

_regexBackTracking.IsMatch("aaaaaaaaaaaaaaaaaaaaa");

Regex_BackTracking .NET 4.8 43,722,439.1 ns

Regex_BackTracking .NET Core 3.1 34,763,742.9 ns

Regex_BackTracking .NET Core 5.0 578.1 ns

Przetestuj sam!

Nic nie stoi na przeszkodzie, aby samemu sprawdzić jak bardzo .NET Core i .NET 5 są szybkie na lokalnej maszynie. Z tego też powodu już jakiś czas temu stworzyłem projekt na GitHubie: https://github.com/djfoxer/DotNetFrameworkVsCore Są tu dostępne kody źródłowe i gotowy plik exe, który porówna wymienione trzy frameworki na Twoim komputerze. Dodatkowo zamieszczam kilka wyników z różnych procesorów od różnych producentów. Warto sprawdzić, na pewno będziesz mile zaskoczony jak faktycznie olbrzymi skok wydajnościowy został uczyniony w .NET Core 3x i .NET 5. 

DotNetFrameworkVsCore - https://github.com/djfoxer/DotNetFrameworkVsCore