Jak działają nullowalne typy proste?

0

Cześć,
chciałem się zapytać jak działają nullowalne typy proste oraz czy ich stosowanie ma jakiś wpływ na wydajność programu?
Przykładowo typ byte może przyjmować wartości od 0 do 255 (2^8), natomiast typ byte? może przyjmować wartości od 0 do 255 + null - czyli albo takie zmienne zajmują więcej pamięci(?).
No i teraz pytanie jak program radzi sobie z takim typem - skąd wie czy dana zmienna ma wartość null?
Czy program ma jakąś wewnętrzną tablicę ze zmiennymi, które są nullami czy jest to inaczej rozwiązane?

2

Te typy to tak naprawdę struct Nullable<T> https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1?view=net-7.0
Tutaj przykładowo jego kod z .NET Framework https://referencesource.microsoft.com/#mscorlib/system/nullable.cs jak widać ma dodatkowe pole hasValue.

0

Ok, czyli np. typ int zajmuje 4 byte'y a int? zajmuje 5 byte'ów (4 + 1 na bool) tak?

8

prawie na pewno nie, zazwyczaj dane w pamięci są wyrównywane przed padding więc ten bool będzie dopełniony do 4 bajtów i w sumie int? zajmie prawdopodobnie 8 bajtów, ale to może zależeć od architektury procesora, pamięci, a nawet wersji .NETa więc ciężko jednoznacznie odpowiedzieć. Z tego co można samemu przetestować int? na 64 bitowym procesorze zajmuje 8 bajtów, natomiast na przykład bool? zajmuje 2 bajty bo rozmiar struktury w obu przypadkach został wyrównany tak żeby wszystkie pola dało się adresować przez proste mnożenie ich rozmiarów bez dodawania i tak żeby mieściły się w jednej komórce pamięci. Ma znaczenie nawet ułożenie pól w strukturze, więc np:

struct Test
{
    bool a; // 1 bajt
    // 7 bajtów wyrównania
    long b; // 8 bajtów
    bool c; // 1 bajt
    bool d; // 1 bajt
    // 6 bajtów wyrównania
}

taki twór zajmie w pamięci 24 bajty bo bool a jest dorównane w pamięci do 8 (sic!) bajtów żeby łatwiej można było zaadresować long b, natomiast jeśli zmienisz ułożenie pól na takie:

struct Test
{
    bool a; // 1 bajt
    bool c; // 1 bajt
    bool d; // 1 bajt
    // 5 bajtów wyrównania
    long b; // 8 bajtów
}

to struktura zajmie tylko 16 bajtów.

Można to zachowanie zmienić przez dodanie atrybutu
[StructLayout(LayoutKind.Auto)] - wtedy kompilator zmieni ustawienie pól automatycznie tak żeby ułożenie było optymalne w pamięci, lub:
[StructLayout(LayoutKind.Sequential, Pack = 1)] żeby wyłączyć wyrównywanie - wtedy obie struktury zajmą jedyne 11 bajtów ale będzie to miało pewne konsekwencje wydajnościowe.

Bez wyrównywania pole long będzie leżało na 4 komórce pamięci i będzie zajmowało 8 bajtów - jeśli masz tablicę takich struktur to żeby zaadresować trzeci taki long bez paddingu, trzeba wykonać obliczenie 11 * 2 + 3. Przy domyślnym wyrównaniu, żeby zaadresować trzeci long trzeba tylko policzyć 8 * 7 (8 bajtów na longa i każda struktura zajęła 3 takie pakiety + long jest na drugiej pozycji stąd 3 + 3 + 1 = 7) a więc odpada jakiekolwiek dodawanie przy adresowaniu.
Dodatkowo pamięć musi i może być zazwyczaj odczytywana tylko całymi "słowami", jeśli pole leży na granicy dwóch obszarów to trzeba odpytać dwa obszary pamięci zamiast jednego żeby odczytać jego wartość co może mieć poważne konsekwencje wydajnościowe.

Podsumowując - T? aka Nullable<T> zajmuje w pamieci zazwyczaj 2x więcej pamięci niż T. Jeśli ma to dla ciebie znaczenie bo masz dużą tablicę takich elementów to możesz to zoptymalizować przez trzymanie osobno tablicy boolean "hasValue" i osobno tablicy wartości - to powinno być optymalne wydajnościowe i pamięciowo. Idąc dalej w jednym bajcie można trzymać informację nie o jednej ale aż o 8 wartościach, można użyć klasy BitArray żeby jeszcze bardziej upchać dane w pamięci.

0

Super! Dziękuję za tak obszerne wyjaśnienie ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1