StructLayout – wprowadzenie

Zanim przejdę do wyjaśniania po co został wprowadzony atrybut StructLayout, najpierw wyjaśnię jak pola w strukturach danych są rozmieszczane w pamięci. Weźmy na przykład taką strukturę:

struct SampleStruct
{
   public byte OneByte;
   public int FourBytes;
   public byte OneByte1;
}

Ile pamięci powinno zostać zaalokowanej dla powyższej struktury? Może wydawać się, że 6 ponieważ Integer zajmuje 4 a Byte 1. Ze względu na optymalizacje nie jest to takie proste i oczywiste. Na moim komputerze jest to 12. Procesor operuje (w zależności od architektury) na danych o wielkości 4 bajty (32 bity). Z tego względu najszybciej dla niego jest odczytać fragmenty znajdujące się na pozycji 4n. Gdyby powyższa struktura zajmowała 6 bajtów wtedy Integer zaczynałby się od drugiego bajta. Aby procesor mógł odczytać taką zmienną musiałby przeczytać dwa kawałki pamięci (pamiętamy, że CPU operuje na 4 bajtowych fragmentach). Prosta optymalizacja polega zatem na wypełnieniu struktury pustymi bajtami. Tak więc OneByte zajmuje nie jeden bajt a cztery. Podobnie sprawa wygląda z ostatnim polem. Dzięki temu, CPU może za jednym razem odczytać każdą ze zmiennych – nie ma konieczności czytania dwóch fragmentów pamięci.

Nie zawsze jednak chcemy takiej optymalizacji. StructLayout daje nam większą kontrolę jak pola struktury będą wypełniane lub porządkowane. Parametr LayoutKind  posiada 3 wartości:

  1. Sequential – domyślna wartość. Pola są szeregowane jeden po drugim. Jeśli OneByte jest pierwszy w kolejności to i w pamięci będzie jako pierwszy.
  2. Explicit – pozwala uporządkować dowolnie pola. Programista może samodzielnie określić gdzie dane pola powinny zaczynać się.
  3. Auto – nie mamy żadnego wpływu  i wszystko zostanie  automatycznie ustalone przez środowisko uruchomieniowe.

Dla C# Sequential jest domyślną wartością ale oprócz LayoutKind można ustalić tzw. Pack. Zobaczmy przykład:

[StructLayout(LayoutKind.Sequential,Pack = 1)]
struct SampleStruct
{
   public byte OneByte;
   public int FourBytes;
   public byte OneByte1;
}

Pack określa wspomniane wypełnianie. Pack równy jeden znaczy, że pola mogą mieć minimalnie jeden bajt. W praktyce oznacza to, że SampleStruct będzie miał rozmiar 6 – łatwo sprawdzić za pomocą funkcji Sizeof:

Console.WriteLine(Marshal.SizeOf(new SampleStruct()));

Analogicznie, Pack=2 spowoduje, że struktura zostanie upakowana do 8 bajtów:

class Program
{
    [StructLayout(LayoutKind.Sequential,Pack = 2)]
    struct SampleStruct
    {
        public byte OneByte;
        public int FourBytes;
        public byte OneByte1;
    }

    static void Main(string[] args)
    {
        // 8 bajtów
        Console.WriteLine(Marshal.SizeOf(new SampleStruct()));
    }
}

A co się stanie z poniższą deklaracją (Pack=3)?

[StructLayout(LayoutKind.Sequential,Pack = 3)]
struct SampleStruct
{
   public byte OneByte;
   public int FourBytes;
   public byte OneByte1;
}

Powyższy kod nawet się nie skompiluje i dostaniemy następujący błąd:

System.Runtime.InteropServices.StructLayoutAttribute' attribute -- 'Incorrect argument value.'

.NET akceptuje wyłącznie następujące wartości Pack: 0, 1, 2, 4, 8, 16, 32, 64,128. Dla innych liczb, kompilacja po prostu będzie niemożliwa. Zero ( 0 ) jest wartością domyślną i oznacza, że pakowanie będzie dokonane przez runtime i zależy od aktualnej platformy. Jeden ( 1 ) w praktyce oznacza, że kolejne pola nie będą miały przerw między sobą – brak wspomnianego wypełniania pustymi bajtami. Pozostałe wartości (2 i większe) oznaczają, że pola będą rozpoczynały się od wielokrotności pola PACK. Zatem jeśli Pack=2 to poszczególne pola mogą zaczynać się od 0,2,4,6,8 itp.  W praktyce wartości wyższe niż 4 lub 8 są po prostu ignorowane ponieważ procesor nie operuje na tak dużych porcjach danych. Poniższy kod wciąż zwraca 12:

[StructLayout(LayoutKind.Sequential,Pack = 128)]
struct SampleStruct
{
   public byte OneByte;
   public int FourBytes;
   public byte OneByte1;
}

static void Main(string[] args)
{
   // PACK został zignorowany - niezgodny z architekturą aktualnego CPU
   Console.WriteLine(Marshal.SizeOf(new SampleStruct()));
}

Pack można używać zarówno dla Sequential jak i Explicit. Dla Auto poskutkuje to błędem runtime:

class Program
{
    [StructLayout(LayoutKind.Auto,Pack = 4)]
    struct SampleStruct
    {
        public byte OneByte;
        public int FourBytes;
        public byte OneByte1;
    }

    static void Main(string[] args)
    {
        // PACK został zignorowany - niezgodny z architekturą aktualnego CPU
        Console.WriteLine(Marshal.SizeOf(new SampleStruct()));
    }
}

image

Aby przekonać się, że Sequential naprawdę jest wartością domyślną spróbujmy zmienić kolejność pól struktury:

class Program
{
    struct SampleStruct
    {
        public int FourBytes;
        public byte OneByte;
        public byte OneByte1;                
    }

    static void Main(string[] args)
    {
        Console.WriteLine(Marshal.SizeOf(new SampleStruct()));
    }
}

Bez używania Pack, na ekranie wyświetli się wartość 8 –  po drobnej zamianie kolejności nasza struktura ma mniejszy rozmiar. Ucieszy to na pewno osoby lubiące optymalizować wszystko co się daSmile Dodam, że jest to znacznie lepsze niż używanie parametru Pack – Pack=1 nie jest dobrym rozwiązaniem ze względu na specyfikę pracy CPU i jego operowanie na porcjach danych (słowach).

Ostatnią wartością jest Explicit. Daje ona największe możliwości jeśli chodzi o alokację pamięci. Jeśli zdecydujemy się na Explicit, sami musimy za pomocą atrybutu FieldOffset określić gdzie dane pole zaczyna się w pamięci. Aby otrzymać rozmiar 8 bez wspomnianej wyżej zmiany kolejności pól wystarczy:

class Program
{
    [StructLayout(LayoutKind.Explicit)]
    struct SampleStruct
    {
        [FieldOffset(0)]
        public byte OneByte1;
        [FieldOffset(2)]
        public int FourBytes;
        [FieldOffset(1)]
        public byte OneByte;        
    }

    static void Main(string[] args)
    {
        // 8
        Console.WriteLine(Marshal.SizeOf(new SampleStruct()));
    }
}

Brak atrybutu FieldOffset spowoduje błąd na etapie kompilacji – jest to po prostu niezbędne dla Explicit. Powyższy przykład chyba jest jasny: pola Byte zaczynają się od 0 i 1 a Integer od 2. Oczywiście lepiej użyć Sequential i zmianę kolejności pól lub atrybut Pack. Nie zawsze się jednak tak się da ale o tym w następnym poście.

W dzisiejszym poście  było dość teoretycznie, w zasadzie bez praktycznego przykładu. W następnym wpisie  wyjaśnię dlaczego StructLayot został dostarczony i kiedy faktycznie z niego należy korzystać. Po dzisiejszym poście kwestia API powinna być już jasna.

2 thoughts on “StructLayout – wprowadzenie”

Leave a Reply

Your email address will not be published.