25 lutego 2009

Istotna kolejność

Dzięki operatorowi sizeof() możemy dowiedzieć się jaki jest rozmiar danego typu w bajtach. Standard świadomie nie określa odgórnych rozmiarów typów podstawowych, zostawiając to zależnym od platformy sprzętowej (opisane są tylko relacje wielkości pomiędzy typami). Mimo, że wszystkie produkowane dziś procesory operują na słowach 64 bitowych, to jednak znaczna większość użytkowników posiada 32 bitowe systemy operacyjne, dla których tworzony jest kod aplikacji.

Przypatrzmy się więc dwóm z pozoru identycznym strukturom:

struct A{
    short a1;
    int b1;
    short a2;
};
struct B{
    short a1
    short a2;
    int b1;
};

Mają służyć tylko jako przykład, więc nie są to wyjątkowo praktyczne typy. Jak widzimy obydwie struktury mają po dwa pola typu short i jedno typu int. Zróbmy teraz mały eksperyment i wyświetlmy rozmiary obu tych typów:

printf("%d %d\n",sizeof(A), sizeof(B));

Naszym oczom ukaże się dość zadziwiający widok, bo rozmiary typów A i B są różne, i wynoszą odpowiednio 12 i 8 bajtów (na procesorze z rodziny IA-32). Dość zaskakujące, nieprawdaż? Tylko z pozoru.
Zacznijmy więc od początku. Z prostego dodawania wynika, że obie te struktury powinny zajmować 8 bajtów (2+2+4). Pierwsza z nich jednak, zajmuje o 4 bajty więcej. W tym momencie przyda się podstawowa wiedza na temat działania procesora. Danymi, na których procesory 32 bitowe, pracują z największą wydajnością to właśnie 32 bity. Tyle wynosi rozmiar słowa i rejestrów ogólnego przeznaczenia. Wysłanie większej, lub mniejszej, ilości danych, jest realizowane wolniej, dlatego nie zawsze zaleca się ich używanie. Spójrzmy teraz na strukturę A. Aby efektywnie móc przesłać wartość jej pól do rejestrów, najlepiej zrobić to 32 bitowymi paczkami. Napotykamy jednak na problem. Wedle tej idei, zmienną b1 trzeba wysyłać 2 razy (po 16 bitów wraz z a1 i a2). Nie jest bezpieczne i poprawne rozwiązanie. W takim przypadku kompilatory stosują bardzo prostą sztuczkę: wyrównują zmienne do 32 bitów, co w tym przypadku oznacza, że a1 oraz a2 będą zajmować po 4 bajty. Na każdej z tych zmiennych zyskujemy dodatkowo po 2 bajty, więc zagadka została wyjaśniona. Dlaczego więc struktura B, zajmuje 8 bajtów? Decyduje o tym właśnie ułożenie zmiennych. W tym przypadku zmienne a1 i a2 można wysłać za pomocą jednego wywołania instrukcji mov, więc wyrównanie ich nie jest potrzebne.
Obrazowo można przedstawić to tak, że struktura (a zarazem i klasa) to pojemnik z 32 bitowymi (lub 64, w przypadku procesorów i systemów operacyjnych 64 bitowych) pudełkami, do których są kolejno dodawane zmienne. Jeśli dana zmienna nie zajmuje całego pudełka, a kolejna zmienna już nie mieści się do niego, wolne miejsce zostaje zapełnione – natura nie lubi próżni. :) 

2 komentarze:

  1. Dokładnie to jest tak, że pole jest wyrównywane do takiego adresu, jaki samo ma rozmiar. Na przykład jeśli dasz char a potem short, to short wyrówna się do 2 bajtów, tak jak po short, int wyrównał się do 4.

    Mógłbyś też dodać, że ścisłe upakowanie struktury da się zrobić w C++ dyrektywą:
    #pragma pack(1)

    OdpowiedzUsuń
  2. Hmm... No dla char, short całośc ma 4 bajty, więc to char wyrównuje się do 2, zaś dla short pozostaje standardowo 2.

    OdpowiedzUsuń