Wyślij zapytanie Dołącz do Sii

Podczas pracy ze zbiorami danych często zdarza się nam użyć kolekcji umożliwiających dostęp do danych po kluczu. Użycie słownika pozwala otrzymać wartość ze zbioru przy złożoności obliczeniowej O(1), bez względu na to, jak duża jest dana kolekcja. Najczęściej klucz stanowi prosty typ wartościowy lub łańcuch znaków.

Problem pojawia się, kiedy chcemy użyć wielu wartości, w postaci klucza złożonego. Zwykle sprowadza się to do utworzenia metody generującej łańcuch znaków na podstawie kilku pól, a następnie wykorzystywanie powstałego łańcucha w słownikach. Taki zabieg często jest dla nas wystarczający, ale nie jest to jedyna opcja.

W artykule postaram się przybliżyć możliwości wykorzystania struktur jako kluczy złożonych oraz opiszę, na co musimy zwrócić uwagę, aby nie spowodować spadków wydajności.

Analizowana struktura

Zacznijmy od przedstawienia omawianej struktury. Nasz klucz w słowniku będzie składać się z dwóch wartości, gdzie pierwsza to łańcuch znaków (string), a druga to liczba całkowita (int). Załóżmy, że łańcuch znaków będzie reprezentować kod kraju, natomiast liczba całkowita będzie oznaczać identyfikator subskrypcji. Z unikalnego połączenia kraju oraz subskrypcji uzyskamy informację, jaki dostawca będzie świadczyć usługi.

public struct SupplierKey
{
    public string CountryCode { get; set; }
 
    public int SubscriptionId { get; set; }
}

Następnie będziemy chcieli uzyskać informację o dostawcy dla konkretnego użytkownika, korzystając z poniższego słownika:

Dictionary<SupplierKey, string> SuppliersMapping { get; set; }

Przedstawiony słownik będzie działać, ale działanie to dalece odbiega od wydajnego, a implementację struktury nie możemy określić jako poprawną i zgodną z dobrymi praktykami obowiązującymi w języku C#.

Zanim przejdziemy do zbadania wydajności oraz wskazania i naprawienia błędów musimy najpierw omówić podstawowe zagadnienia związane z łańcuchami znaków, strukturami oraz słownikami.

Łańcuchy znaków

Nasza struktura, będąc typem wartościowym, posiada wewnątrz łańcuch znaków, który jest typem referencyjnym. Jak wiadomo, typy wartościowe porównywane są przez wartość, natomiast typy referencyjne porównywane są przez sprawdzenie czy referencja wskazuje na ten sam adres. Na szczęście łańcuchy znaków w C# mają już nadpisane metody GetHashCode oraz Equals. Dzięki temu nie musimy się przejmować tym, czy dwa obiekty posiadające tą samą wartość będą ze sobą równe.

Equals i GetHashCode dla łańcuchów znaków

Metoda GetHashCode przechodzi znak po znaku i wykonuje na bajtach każdego ze znaków operacje bitowe służące do obliczenia końcowego kodu hash. Podobną rzecz możemy zobaczyć podczas analizy metody Equals, gdzie po typowym sprawdzeniu referencji następuje wywołanie metody EqualsHelper. Wykorzystując Span, metoda pomocnicza, iterując po każdym znaku z ciągu, porównuje jego bajty z bajtami odpowiadającego znaku z drugiego łańcucha.

Musimy również zwrócić uwagę na ważny komentarz znajdujący się nad omawianymi metodami (System.Private.CoreLib/src/System/String.Comparison.cs):

If strings A and B are such that A.Equals(B), then they will return the same hash code.

Jak zobaczymy podczas omawiania kolekcji, uzyskanie takiego samego kodu dla równych sobie obiektów będzie dla nas bardzo ważne. Na razie zanotujmy, że zmiana sposobu porównywania danego typu musi wiązać się zmianą sposobu obliczania kodu hash, tak aby dwie równe sobie instancje zwracały ten sam kod.

Struktury

Rozpoczynając omawianie struktur w .NET, należy zaznaczyć, że stanowią one specjalny konstrukt niejawnie dziedziczący z klasy ValueType. Klasa ta stanowi podstawę, która nadpisuje metody wirtualne z bazowej klasy Object, sprawiając, że ich implementacja jest bardziej dostosowana do potrzeb typów wartościowych.

Musimy również zauważyć, że przez to niejawne dziedziczenie, struktury nie mogą dziedziczyć z żadnej innej klasy bądź struktury. Dodatkowo sama klasa ValueType oznaczona jest jako specjalna i nie może pełnić roli klasy bazowej dla żadnej innej klasy.

Przyglądając się temu, jak działa porównywanie struktur oraz obliczanie ich kodu hash, tak naprawdę będziemy się przyglądać metodom pochodzącym z klasy ValueType. Kod źródłowy dla ValueType jest dostępny pod linkiem (System.Private.CoreLib/src/System/ValueType.cs) i nie jest na pierwszy rzut oka przytłaczający. Plik mierzy raptem 170 linii kodu C#. Kiedy się uważnie przyjrzymy, to zobaczymy, że nie znajduje się tu pełny kod dla omawianych metod. Będziemy analizować kod C#, który odwołuje się do niezarządzanego (ang. Unmanaged) i wysokowydajnego kodu CoreCLR w C++.

Equals w strukturach

Metoda Equals posiada zdefiniowany wysokopoziomowy algorytm, który odwołuje się do kodu C++. Samo porównywanie rozpoczynamy od sprawdzenia typów. Jeśli się nie zgadzają, to od razu wiemy, że nie są sobie równe. Następnie sprawdzamy, czy możliwe jest porównanie po bajtach. Zależy to między innymi od tego, czy wszystkie pola stanowią typy wartościowe. Jeśli występuje co najmniej jedno pole typu referencyjnego, musimy pominąć szybkie sprawdzenie bajtów i przejść dalej.

Istnieje jeszcze kilka innych warunków, które można znaleźć w kodzie C++ (coreclr/vm/comutilnative.cpp). Między innymi sprawdzane jest to, czy Equals albo GetHashCode nie zostały przeładowane. W naszej analizowanej strukturze użyliśmy pola o typie string, więc musimy przystąpić do wolniejszego porównywania.

Dalej wyciągane są wartości wszystkich pól przy użyciu refleksji, które następnie są użyte do ostatecznego porównania struktur. Przyglądając się parametrom oraz używanym typom wszędzie zobaczymy Object, a to oznacza, że nie unikniemy boxingu, również przy porównywaniu każdego z pól. Łącząc ze sobą boxing oraz refleksję otrzymujemy oczywisty powód, przez który możemy uznać tą metodę za niewydajną dla struktur.

GetHashCode w strukturach

Metoda GetHashCode jest nieco bardziej skomplikowana, ale, podobnie jak Equals, posiada do wyboru dwa główne algorytmy – szybki oraz wolny – których wybór oparty jest na tych samych warunkach, co poprzednio. Szybki algorytm oblicza kod hash, opierjąc się na bajtach, natomiast wolniejszy jest oparty na strategii wyliczanej dla konkretnego typu (coreclr/vm/comutilnative.cpp).

Pomijając niepotrzebne nam teraz szczegóły, wybierane jest tylko pierwsze pole z obiektu i wyłącznie to pole używane jest do obliczenia kodu hash. Oznacza to, że dla każdej struktury, która posiada co najmniej jedno pole typu referencyjnego, kod hash będzie taki sam, jeśli pierwsze pole będzie mieć taką samą wartość.

Pierwszy problem – bez nadpisanej metody GetHashCode struktury używają tylko pierwszego pola do obliczenia kodu hash

Informacje uzyskane dzięki analizie kodu źródłowego klasy ValueType stanowią podstawę do naprawy implementacji struktury SupplierKey.

Jak się dowiedzieliśmy, SupplierKey w obecnej formie używa wyłącznie pierwszego pola (kod kraju) do obliczenia kodu hash. Potwierdza to poniższy kod, gdzie zmiana identyfikatora subskrypcji nie powoduje wygenerowania nowego kodu hash.

var key1 = new SupplierKey { CountryCode = "DE", SubscriptionId = 1 };
var key2 = new SupplierKey { CountryCode = "DE", SubscriptionId = 2 };
var key3 = new SupplierKey { CountryCode = "PL", SubscriptionId = 1 };
var key4 = new SupplierKey { CountryCode = "PL", SubscriptionId = 2 };
 
Console.WriteLine(key1.GetHashCode()); // 876931578
Console.WriteLine(key2.GetHashCode()); // 876931578
Console.WriteLine(key3.GetHashCode()); // 1001209387
Console.WriteLine(key4.GetHashCode()); // 1001209387

Oznacza to dla nas, że w przypadku użycia tej struktury jako klucza w słowniku będziemy operować na powtarzających się kodach hash. Ma to znaczenie ze względów wydajnościowych, ponieważ kod hash klucza jest używany do określenia, jaki indeks w wewnętrznej tablicy słownika zajmie wstawiana przez nas wartość.

W przypadku wielu obiektów o takim samym kodzie hash, podczas wyciągania wartości ze słownika nie będziemy mogli wykorzystać wyłącznie indeksu, ale również będziemy musieli przejść przez pozostałe obiekty o takim samym kodzie hash i sprawdzić, o który z nich tak naprawdę nam chodzi. Tym samym, nasza zakładana złożoność obliczeniowa O(1) zamienia się w O(n).

Omawiany kod znajduje się wewnątrz metody Dictionary.FindValue i dostępny jest pod linkiem (System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs).

Naprawa pierwszego problemu

Uzyskaliśmy teraz wystarczający powód do przeładowania metody GetHashCode. Musimy użyć pozostałych pola, tak aby hash obliczany był dla całego klucza, a nie tylko dla jego części. Wykorzystamy do tego metodę HashCode.Combine().

public struct SupplierKey_WithHashCode
{
    public string CountryCode { get; set; }
 
    public int SubscriptionId { get; set; }
 
    public override int GetHashCode()
    {
        return HashCode.Combine(CountryCode, SubscriptionId);
    }
}

Po nadpisaniu metody GetHashCode uzyskujemy inny kod hash przy zmianie wartości dowolnego z pól w naszej strukturze.

var key1 = new SupplierKey_WithHashCode { CountryCode = "DE", SubscriptionId = 1 };
var key2 = new SupplierKey_WithHashCode { CountryCode = "DE", SubscriptionId = 2 };
var key3 = new SupplierKey_WithHashCode { CountryCode = "PL", SubscriptionId = 1 };
var key4 = new SupplierKey_WithHashCode { CountryCode = "PL", SubscriptionId = 2 };
 
Console.WriteLine(key1.GetHashCode()); // 92690337
Console.WriteLine(key2.GetHashCode()); // 535339796
Console.WriteLine(key3.GetHashCode()); // -881525390
Console.WriteLine(key4.GetHashCode()); // 195508225

Pozostaje nam jeszcze sprawdzenie, czy uda nam się uzyskać ten sam kod hash dla takich samych wartości, co potwierdza poniższy kod.

var key1 = new SupplierKey_WithHashCode { CountryCode = "DE", SubscriptionId = 1 };
var key2 = new SupplierKey_WithHashCode { CountryCode = "DE", SubscriptionId = 1 };
 
Console.WriteLine(key1.GetHashCode()); // 1393730686
Console.WriteLine(key2.GetHashCode()); // 1393730686

Pierwsze testy wydajności

W teorii naprawiliśmy problem, ale przeprowadźmy teraz pierwszy test wydajności, sprawdzając, ile czasu zajmie pobranie wartości ze słownika. Będziemy używać trzech zestawów danych stanowiących iloczyn kartezjański dla X krajów oraz Y subskrypcji:

Zestaw 1. X = 50, Y = 50, X×Y = 2 500,

Zestaw 2. X = 100, Y = 100, X×Y = 10 000,

Zestaw 3. X = 200, Y = 200, X×Y = 40 000.

Do przeprowadzenia testów wykorzystamy bibliotekę BenchmarkDotNet oraz .NET 8.0.1. Poniższy kod przedstawia klasę DictionaryGetTests, w której możemy zauważyć następujące segmenty:

  • Pole Size odpowiada za wielkość kolekcji. Jego wartość będzie się zmieniać w zależności od tego, który zestaw danych będzie użyty do testów.
  • Metoda IterationSetup wywoływana jest przed każdym testem i ma za zadanie zainicjalizować słowniki oraz wstawić do nich wartości umożliwiające przeprowadzenie testów. Kod znajdujący się w tej metodzie nie zostanie uwzględniony podczas pomiarów wydajności.
  • Metody DictionaryGet_WithoutHashCode oraz DictionaryGet_WithHashCode stanowią testowany blok kodu. To w nich następuje iteracja po wszystkich krajach i subskrypcjach oraz pobranie wartości z odpowiedniego słownika. Dla DictionaryGet_WithoutHashCode klucz nie posiada nadpisanej metody GetHashCode, a dla DictionaryGet_WithHashCode używamy klucza z nadpisaną metodą GetHashCode.
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
 
[MemoryDiagnoser(true)]
public class DictionaryGetTests
{
    // Wielkość kolekcji
    [Params(50, 100, 200)]
    public int Size;
 
    // Testowane słowniki
    private Dictionary<SupplierKey, string> _suppliersMapping_WithoutHashCode;
    private Dictionary<SupplierKey_WithHashCode, string> _suppliersMapping_WithHashCode;
 
    // Dostępne klucze
    private SupplierKey[] _supplierKeys_WithoutHashCode;
    private SupplierKey_WithHashCode[] _supplierKeys_WithHashCode;
 
    // Wartość dla każdego z kluczy w słowniku jest niezmienna
    private const string Value = "supplier";
 
    // Metoda przygotowująca dane testowe 
    [IterationSetup]
    public void IterationSetup()
    {
        _suppliersMapping_WithoutHashCode = new Dictionary<SupplierKey, string>(Size*Size);
        _suppliersMapping_WithHashCode = new Dictionary<SupplierKey_WithHashCode, string>(Size*Size);
 
        for (int code = 0; code < Size; code++)
        {
            var countryCode = ((char)code).ToString();
 
            for (int subscriptionId = 0; subscriptionId < Size; subscriptionId++)
            {
                // Do dwóch słowników umieszczamy klucz o takich samych wartościach, ale innym typie

                _suppliersMapping_WithoutHashCode.Add(new SupplierKey
                {
                    CountryCode = countryCode,
                    SubscriptionId = subscriptionId
                }, Value);
 
                _suppliersMapping_WithHashCode.Add(new SupplierKey_WithHashCode
                {
                    CountryCode = countryCode,
                    SubscriptionId = subscriptionId
                }, Value);            
            }
        }
 
        _supplierKeys_WithoutHashCode = _suppliersMapping_WithoutHashCode.Keys.ToArray();
        _supplierKeys_WithHashCode = _suppliersMapping_WithHashCode.Keys.ToArray();    
    }
 
    // Test przeprowadzany dla słownika opartego o klucze w postaci niezmienionej struktury
    [Benchmark]
    public void DictionaryGet_WithoutHashCode()
    {
        foreach (var key in _supplierKeys_WithoutHashCode)
        {
            _suppliersMapping_WithoutHashCode.TryGetValue(key, out _);
        }
    }

     // Test przeprowadzany dla słownika opartego o klucze w postaci struktury z nadpisaną metodą GetHashCode
    [Benchmark]
    public void DictionaryGet_WithHashCode()
    {
        foreach (var key in _supplierKeys_WithHashCode)
        {
            _suppliersMapping_WithHashCode.TryGetValue(key, out _);
        }
    }
}

Wyniki pierwszych testów wydajności

Po wywołaniu powyższego kodu uzyskaliśmy wyniki przedstawione na Ryc. 1.

Wyniki pierwszych testów wydajności
Ryc. 1 Wyniki pierwszych testów wydajności

Od razu możemy wyciągnąć następujące wnioski:

  • Poprzez nadpisanie metody GetHashCode udało nam się zredukować czas potrzebny na wykonanie testowanego bloku kodu.
  • Czas wykonania DictionaryGet_WithHashCode rośnie w sposób zbliżony do liniowego. Dla 40 000 operacji stanowi on 4-krotność czasu potrzebnego na wykonanie 10 000 operacji, gdzie dla DictionaryGet_WithoutHashCode jest to ponad 7 razy więcej.
  • Zarówno dla DictionaryGet_WithHashCode jak i DictionaryGet_WithoutHashCode, widzimy dużą liczbę bajtów zaalokowanych na stercie, która rośnie wraz z powiększaniem się liczby wykonywanych operacji.

Po szybkim przeanalizowaniu wyników widzimy, że udało nam się uzyskać zakładany rezultat, ale nadal jest miejsce do poprawy. Najbardziej zauważalna jest ilość alokowanej pamięci. Stanowi to kolejny problem, jaki będziemy rozwiązywać.

Drugi problem – niepotrzebne alokacje pamięci

Żeby zrozumieć problem związany z alokacjami, musimy znowu przyjrzeć się kodowi źródłowemu słowników, a konkretniej sposobowi, w jaki są one ze sobą porównywane.

Za znalezienie klucza w słowniku odpowiedzialna jest metoda Dictionary.FindValue, która przeszukuje wewnętrzną tablicę słownika i stara się zwrócić wartość odpowiadającą wprowadzonemu kluczowi. Poniższy kod przedstawia warunek pochodzący z Dictionary.FindValue (System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs).

// Kod wywoływany dla typów wartościowych bez zdefiniowanego comparera
if (typeof(TKey).IsValueType && comparer == null)
{
    // kod pominięty na potrzeby artykułu

         // Do porównania użyty zostaje EqualityComparer<TKey>.Default
    if (entry.hashCode == hashCode && EqualityComparer<TKey>.Default.Equals(entry.key, key))
    {
        goto ReturnFound;
    }

    // kod pominięty na potrzeby artykułu
}

Jak widzimy, dla typów wartościowych używany jest domyślny EqualityComparer, ale żeby zrozumieć co to oznacza, musimy przejść dalej do metody CreateDefaultEqualityComparer, która wywoływana jest z EqualityComparer<TKey>.Default (System.Private.CoreLib/src/System/Collections/Generic/ComparerHelpers.cs).

internal static object CreateDefaultEqualityComparer(Type type)
{
    // kod pominięty na potrzeby artykułu

    // Warunek sprawdzający czy klucz w słowniku implementuje IEquatable<>
    else if (type.IsAssignableTo(typeof(IEquatable<>).MakeGenericType(type)))
    {
        // Zwracany jest wydajny GenericEqualityComparer
        result = CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<string>), runtimeType);
    }

    // kod pominięty na potrzeby artykułu

    // Zwracany jest niewydajny ObjectEqualityComparer
    return result ?? CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ObjectEqualityComparer<object>), runtimeType);
}

Jak możemy zauważyć, jeśli klucz w słowniku jest typem implementującym interfejs IEquatable<>, używany jest GenericEqualityComparer oparty na interfejsie IEquatable<>. W przeciwnym wypadku słownik używać będzie wolniejszego ObjectEqualityComparer, który podczas porównywania musi rzutować nasz typ wartościowy na obiekt, powodując tym samym niepotrzebne alokacje pamięci.

Naprawa drugiego problemu

Teraz, kiedy wiemy, co powoduje dodatkowe alokacje, możemy przystąpić do zmian w kodzie. Musimy zaimplementować interfejs IEquatable<> wraz z metodą Equals. Poniższy kod przedstawia strukturę SupplierKey_WithHashCode_AndEquals. Zauważmy, że nadpisaliśmy też metodę Equals pochodzącą z bazowego obiektu, aby odpowiadała tej pochodzącej z IEquatable.

public struct SupplierKey_WithHashCode_AndEquals : IEquatable<SupplierKey_WithHashCode_AndEquals>
{
    public string CountryCode { get; set; }
 
    public int SubscriptionId { get; set; }
 
    // Nadpisana metoda Equals operująca na obiekcie
    public override bool Equals(object? other)
    {
        if(other is SupplierKey_WithHashCode_AndEquals key)
        {
            return Equals(key);
        }
 
        return false;
    }
 
    // Zaimplementowana metoda Equals pochodząca z IEquatable<>
    public bool Equals(SupplierKey_WithHashCode_AndEquals other)
    {
        return CountryCode == other.CountryCode && SubscriptionId == other.SubscriptionId;
    }
 
    public override int GetHashCode()
    {
        return HashCode.Combine(CountryCode, SubscriptionId);
    }
}

Testy wydajności po zaimplementowaniu IEquatable<>

Po wprowadzeniu poprawek możemy przejść do kolejnego testu wydajności, którego wyniki zostały przedstawione na Ryc. 2. Najnowsze zmiany zauważymy w testach metody DictionaryGet_WithHashCode_AndEquals, która bazuje na kluczach w postaci SupplierKey_WithHashCode_AndEquals.

Wyniki testów wydajności po zaimplementowaniu IEquatable<>
Ryc. 2 Wyniki testów wydajności po zaimplementowaniu IEquatable<>

Przeanalizujmy teraz uzyskane czasy oraz alokacje:

  • Zaimplementowanie interfejsu IEquatable pozwoliło nam przyspieszyć działanie pobierania wartości ze słownika. Działa to teraz około 6 razy szybciej przy zachowaniu liniowego wzrostu czasu w stosunku do zwiększania się ilości elementów w słowniku.
  • Drastycznie spadła liczba zaalokowanych bajtów w pamięci. Nie zmienia się ona wraz ze wzrostem wielkości kolekcji. Widoczne jest zaledwie 400 bajtów, które alokowane są przez samą bibliotekę BenchmarkDotNet (więcej informacji pod linkiem). Udało nam się tym samym wyeliminować boxing typów wartościowych podczas pobierania wartości ze słownika.

Dzięki drobnym zmianom udało nam się uzyskać kod, który nie tylko jest wydajny z punktu widzenia czasu potrzebnego na wykonanie obliczeń, ale również nie powoduje dodatkowych alokacji pamięci. Teraz możemy uznać, że nasza struktura została prawidłowo zaimplementowana.

Porównanie omawianej struktury do odpowiednika jako record struct

Przed wprowadzeniem C# 10 pozostalibyśmy ze strukturą, którą opisaliśmy pod koniec poprzedniego podpunktu, jednak coś się zmieniło. Wraz z C# 10 i .NET 6 zostały wprowadzone typy record struct. Posiadają one już wbudowane usprawnienia, które kolejno nanosiliśmy na SupplierKey, więc możemy sprawdzić, jak będą się zachowywać dla naszego przypadku testowego.

Zacznijmy od zdefiniowania nowej struktury SupplierKey_Record, która posiada te same pola, co nasza dotychczasowa struktura i jest oznaczona słowem kluczowym record. Dodatkowo wykorzystamy możliwości zdefiniowania pól bezpośrednio w deklaracji typu, co zostało przedstawione na poniższym kodzie.

public record struct SupplierKey_Record(string CountryCode, int SubscriptionId) { }

Jak widać, różnica w poziomie skomplikowania kodu jest ogromna. Wystarczy jedna linia, żeby zapewnić nam wszystko, co musieliśmy napisać ręcznie.

Testy wydajności dla record struct

Teraz możemy przejść do testów wydajności. Metodę wykorzystującą klucze w postaci typu SupplierKey_Record nazwiemy DictionaryGet_Record. Wyniki testów wydajności dla record struct zostały przedstawione na Ryc. 3.

Testy wydajności dla record struct
Ryc. 3 Testy wydajności dla record struct

Widzimy, że record struct jest szybszy od naszej struktury i udało nam się uzyskać jeszcze krótszy czas potrzebny na pobranie wartości ze słownika. Zauważmy również, że DictionaryGet_Record podobnie jak DictionaryGet_WithHashCode_AndEquals nie alokuje pamięci oraz że wzrost liczby elementów powoduje liniowy wzrost czasu wykonania.

Moglibyśmy dalej wprowadzać zmiany do naszych metod GetHashCode oraz Equals, aby uzyskać zbliżone wyniki, jednak na tym poprzestaniemy.

Udało się nam doprowadzić do sytuacji, gdzie nasza struktura jest wydajna, nie powoduje niepotrzebnych alokacji oraz czas potrzebny na pobranie pojedynczej wartości nie zmienia się w zależności od liczby elementów w słowniku. Przeprowadzając dokładniejsze pomiary z wykorzystaniem profilerów, czy próbując dobrać lepiej działający sposób obliczania kodu hash, moglibyśmy zbliżyć się do recordu, ale mija się to z naszym celem.

Podsumowanie

Podsumowanie

Analizując stosunkowo prosty przypadek, jakim jest zwyczajna struktura z dwoma polami, udało nam się przerobić spory kawałek kodu źródłowego .NET. Zdołaliśmy dowiedzieć się czegoś więcej na temat wewnętrznej implementacji struktur oraz powodów, dla których powinniśmy przestrzegać powszechnie obowiązujących zasad projektowania kodu C#. Poprzez implementację jednego interfejsu oraz nadpisanie odpowiednich metod uzyskaliśmy 300-krotny wzrost wydajności.

Udało nam się też wykorzystać jedną z nowinek z ostatnich lat w postaci record struct, co umożliwiło nam znaczne uproszczenie kodu naszej struktury i osiągnięcie jeszcze krótszego czasu potrzebnego na pobranie wartości ze słownika. Zachęca to nas do śledzenia zmian wprowadzanych do nowych wersji .NET oraz utwierdza w przekonaniu, że rozwój platformy zmierza w dobrym kierunku.

***

Jeśli interesuje Cię C#, zajrzyj również do innych artykułów naszych specjalistów.

4.1/5 ( głosy: 10)
Ocena:
4.1/5 ( głosy: 10)
Autor
Avatar
Aleksander Ligęza

Pełen pasji programista full-stack z ponad 6-letnim doświadczeniem komercyjnym. Do Sii dołączył w 2020 roku, gdzie przez lata jego zaangażowanie, wiedza i umiejętność pracy w zwinnym zespole pozwoliły mu dostarczać produkty wysokiej jakości. W wolnym czasie lubi gotować, czytać i uczyć się języków obcych

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?