Dlaczego sizeof
operator zwraca rozmiar struktury większy niż całkowite rozmiary elementów konstrukcji?
Dlaczego sizeof
operator zwraca rozmiar struktury większy niż całkowite rozmiary elementów konstrukcji?
Odpowiedzi:
Wynika to z dodania wypełnienia w celu spełnienia ograniczeń wyrównania. Wyrównanie struktury danych wpływa zarówno na wydajność, jak i poprawność programów:
SIGBUS
).Oto przykład z typowymi ustawieniami procesora x86 (wszystkie używane tryby 32- i 64-bitowe):
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
Można zminimalizować rozmiar struktur, sortując elementy według wyrównania (wystarczające jest sortowanie według rozmiaru w typach podstawowych) (podobnie jak struktura Z
w powyższym przykładzie).
WAŻNA UWAGA: Zarówno standardy C, jak i C ++ stwierdzają, że wyrównanie struktury jest zdefiniowane w ramach implementacji. Dlatego każdy kompilator może wybrać inne wyrównanie danych, co skutkuje odmiennymi i niekompatybilnymi układami danych. Z tego powodu, mając do czynienia z bibliotekami, które będą używane przez różne kompilatory, ważne jest, aby zrozumieć, w jaki sposób kompilatory wyrównują dane. Niektóre kompilatory mają ustawienia wiersza polecenia i / lub specjalne #pragma
instrukcje do zmiany ustawień wyrównania struktury.
Pakowanie i wyrównanie bajtów, zgodnie z opisem w C FAQ tutaj :
To jest do wyrównania. Wiele procesorów nie ma dostępu do 2- i 4-bajtowych liczb (np. Int i long ints), jeśli są one zatłoczone we wszystkie strony.
Załóżmy, że masz taką strukturę:
struct { char a[3]; short int b; long int c; char d[3]; };
Teraz możesz pomyśleć, że powinna istnieć możliwość spakowania tej struktury do pamięci w następujący sposób:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Ale na procesorze jest o wiele łatwiej, jeśli kompilator tak to zorganizuje:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
W wersji spakowanej zwróć uwagę, że co najmniej trochę trudno jest mi zobaczyć, jak pola b i c owijają się wokół siebie? Krótko mówiąc, jest to również trudne dla procesora. Dlatego większość kompilatorów wypełnia strukturę (jakby dodatkowymi niewidocznymi polami) w następujący sposób:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
wtedy &s.a == &s
i &s.d == &s + 12
(biorąc pod uwagę wyrównanie pokazane w odpowiedzi). Wskaźnik jest przechowywany tylko wtedy, gdy tablice mają zmienny rozmiar (np. a
Został zadeklarowany char a[]
zamiast char a[3]
), ale wtedy elementy muszą być przechowywane gdzie indziej.
Jeśli chcesz, aby struktura miała określony rozmiar w GCC, na przykład użyj __attribute__((packed))
.
W systemie Windows można ustawić wyrównanie na jeden bajt, gdy używany jest komparator cl.exe z opcją / Zp .
Zwykle procesorowi łatwiej jest uzyskać dostęp do danych stanowiących wielokrotność 4 (lub 8), zależnie od platformy, a także od kompilatora.
Zasadniczo jest to kwestia dostosowania.
Musisz mieć dobre powody, aby to zmienić.
Może to być spowodowane wyrównaniem bajtów i dopełnianiem, dzięki czemu struktura wychodzi na parzystą liczbę bajtów (lub słów) na twojej platformie. Na przykład w C w systemie Linux następujące 3 struktury:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
Członkowie, których rozmiary (w bajtach) wynoszą odpowiednio 4 bajty (32 bity), 8 bajtów (2x 32 bity) i 1 bajt (2 + 6 bitów). Powyższy program (w systemie Linux za pomocą gcc) drukuje rozmiary jako 4, 8 i 4 - w których ostatnia struktura jest wypełniona, tak że jest to pojedyncze słowo (4 x 8 bitów na mojej 32-bitowej platformie).
oneInt=4
twoInts=8
someBits=4
:2
i :6
faktycznie określają 2 i 6 bitów, a nie pełne 32-bitowe liczby całkowite w tym przypadku. someBits.x, będąc tylko 2 bitami, może przechowywać tylko 4 możliwe wartości: 00, 01, 10 i 11 (1, 2, 3 i 4). Czy to ma sens? Oto artykuł na temat funkcji: geeksforgeeks.org/bit-fields-c
Zobacz też:
dla Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
i zgodność deklaracji GCC z kompilatorem Microsoft:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
Oprócz poprzednich odpowiedzi należy pamiętać, że niezależnie od opakowania, nie ma gwarancji członkostwa w C ++ . Kompilatory mogą (i na pewno tak robią) dodawać wirtualny wskaźnik tabeli i elementy struktur podstawowych do struktury. Standard nie zapewnia nawet istnienia wirtualnej tabeli (implementacja mechanizmu wirtualnego nie jest określona) i dlatego można stwierdzić, że taka gwarancja jest po prostu niemożliwa.
Jestem całkiem pewien, że kolejność członków jest gwarantowana w C , ale nie liczyłbym na to, pisząc program międzyplatformowy lub kompilator.
Rozmiar struktury jest większy niż suma jej części z powodu tak zwanego upakowania. Określony procesor ma preferowany rozmiar danych, z którym współpracuje. Preferowany rozmiar większości nowoczesnych procesorów to 32 bity (4 bajty). Dostęp do pamięci, gdy dane znajdują się na tego rodzaju granicy, jest bardziej wydajny niż rzeczy, które przekraczają granicę tego rozmiaru.
Na przykład. Rozważ prostą strukturę:
struct myStruct
{
int a;
char b;
int c;
} data;
Jeśli maszyna jest maszyną 32-bitową, a dane są wyrównane na 32-bitowej granicy, widzimy bezpośredni problem (zakładając brak wyrównania struktury). W tym przykładzie załóżmy, że dane struktury zaczynają się od adresu 1024 (0x400 - zauważ, że najniższe 2 bity to zero, więc dane są wyrównane do 32-bitowej granicy). Dostęp do danych. A będzie działał dobrze, ponieważ zaczyna się na granicy - 0x400. Dostęp do data.b również będzie działał dobrze, ponieważ ma on adres 0x404 - kolejna 32-bitowa granica. Ale niezaangażowana struktura umieściłaby data.c pod adresem 0x405. 4 bajty danych. C są w 0x405, 0x406, 0x407, 0x408. Na maszynie 32-bitowej system odczytuje dane. C podczas jednego cyklu pamięci, ale otrzymuje tylko 3 z 4 bajtów (4 bajt znajduje się na następnej granicy). Tak więc system musiałby wykonać drugi dostęp do pamięci, aby uzyskać 4. bajt,
Teraz, jeśli zamiast wstawić data.c pod adresem 0x405, kompilator dopełnił strukturę o 3 bajty i umieścił data.c pod adresem 0x408, wówczas system potrzebowałby tylko 1 cyklu na odczyt danych, skracając czas dostępu do tego elementu danych o 50%. Padding zamienia wydajność pamięci na wydajność przetwarzania. Biorąc pod uwagę, że komputery mogą mieć ogromne ilości pamięci (wiele gigabajtów), kompilatory uważają, że zamiana (prędkość ponad rozmiar) jest rozsądna.
Niestety ten problem staje się zabójczy, gdy próbujesz wysyłać struktury przez sieć lub nawet zapisywać dane binarne do pliku binarnego. Wypełnienie wstawiane między elementy struktury lub klasy może zakłócać przesyłanie danych do pliku lub sieci. Aby napisać kod przenośny (taki, który trafi do kilku różnych kompilatorów), prawdopodobnie będziesz musiał uzyskać dostęp do każdego elementu struktury osobno, aby zapewnić właściwe „pakowanie”.
Z drugiej strony różne kompilatory mają różne możliwości zarządzania pakowaniem struktury danych. Na przykład w Visual C / C ++ kompilator obsługuje komendę #pragma pack. Umożliwi to dostosowanie pakowania i wyrównania danych.
Na przykład:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Powinienem mieć teraz długość 11. Bez pragmy, mógłbym mieć wszystko od 11 do 14 (a dla niektórych systemów aż do 32), w zależności od domyślnego pakowania kompilatora.
#pragma pack
. Jeśli członkowie zostaną przydzieleni w ramach domyślnego wyrównania, ogólnie powiedziałbym, że struktura nie jest zapakowana.
Może to zrobić, jeśli domyślnie lub jawnie ustawiłeś wyrównanie struktury. Strukturę, która jest wyrównana 4, zawsze będzie wielokrotnością 4 bajtów, nawet jeśli rozmiar jej elementów byłby czymś, co nie jest wielokrotnością 4 bajtów.
Również biblioteka może być skompilowana pod x86 z 32-bitowymi intami i możesz porównywać jej komponenty w procesie 64-bitowym, dałbyś inny wynik, gdybyś robił to ręcznie.
Wersja standardowa C99 N1256
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 Wielkość operatora :
3 Po zastosowaniu do operandu, który ma strukturę lub typ unii, wynikiem jest całkowita liczba bajtów w takim obiekcie, w tym dopełnienie wewnętrzne i końcowe.
6.7.2.1 Specyfikacje struktury i związków :
13 ... W obiekcie struktury może znajdować się nienazwane wypełnienie, ale nie na jego początku.
i:
15 Na końcu konstrukcji lub połączenia może znajdować się nienazwane wypełnienie.
Nowa funkcja elastycznego elementu tablicy C99 ( struct S {int is[];};
) może również wpływać na dopełnianie:
16 W szczególnym przypadku ostatni element struktury z więcej niż jednym nazwanym elementem może mieć niepełny typ tablicy; nazywa się to elastycznym elementem tablicy. W większości sytuacji elastyczny element tablicy jest ignorowany. W szczególności rozmiar struktury jest taki, jakby elastyczny element matrycowy został pominięty, z wyjątkiem tego, że może on mieć więcej wypełniania końcowego, niż wynikałoby z pominięcia.
Załącznik J Zagadnienia dotyczące przenośności przypomina:
Następujące nie są określone: ...
- Wartość bajtów dopełniania podczas przechowywania wartości w strukturach lub związkach (6.2.6.1)
Wersja standardowa C ++ 11 N3337
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Rozmiar :
2 Po zastosowaniu do klasy wynikiem jest liczba bajtów w obiekcie tej klasy, w tym wszelkie wypełnienia wymagane do umieszczenia obiektów tego typu w tablicy.
9.2 Członkowie klasy :
Wskaźnik do obiektu struktury o standardowym układzie, odpowiednio przekonwertowany za pomocą reinterpret_cast, wskazuje na jego początkowy element członkowski (lub jeśli ten element jest polem bitowym, a następnie na jednostkę, w której się znajduje) i odwrotnie. [Uwaga: W związku z tym może być nienazwane wypełnienie w obiekcie struktury o standardowym układzie, ale nie na jego początku, co jest konieczne do osiągnięcia odpowiedniego wyrównania. - uwaga końcowa]
Znam wystarczająco dużo C ++, aby zrozumieć notatkę :-)
Oprócz innych odpowiedzi, struct może (ale zwykle nie) ma funkcji wirtualnych, w którym to przypadku rozmiar struktury będzie również zawierał przestrzeń dla vtbl.
Język C pozostawia kompilatorowi pewną swobodę w zakresie lokalizacji elementów strukturalnych w pamięci:
Język C zapewnia programistom pewne zapewnienie układu elementów w strukturze:
Problemy związane z wyrównaniem elementów:
Jak działa wyrównanie:
ps Bardziej szczegółowe informacje są dostępne tutaj: „Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)”
Chodzi o to, że ze względu na szybkość i pamięć podręczną operandy powinny być odczytywane z adresów dopasowanych do ich naturalnego rozmiaru. Aby tak się stało, kompilator wstawia elementy struktury, tak aby następujący element lub następna struktura zostały wyrównane.
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
Architektura x86 zawsze była w stanie pobrać źle wyrównane adresy. Jest jednak wolniejszy i gdy niewspółosiowość nakłada się na dwie różne linie pamięci podręcznej, wówczas eksmituje dwie linie pamięci podręcznej, gdy wyrównany dostęp spowoduje tylko jedną.
Niektóre architektury faktycznie muszą wychwytywać źle ustawione odczyty i zapisy, a także wczesne wersje architektury ARM (tej, która ewoluowała we wszystkie dzisiejsze mobilne procesory) ... cóż, w rzeczywistości po prostu zwróciły złe dane. (Zignorowali bity niskiego rzędu).
Na koniec zauważ, że linie pamięci podręcznej mogą być dowolnie duże, a kompilator nie próbuje zgadywać ich ani dokonywać kompromisu między prędkością a przestrzenią. Zamiast tego decyzje dotyczące wyrównania są częścią ABI i reprezentują minimalne wyrównanie, które ostatecznie równomiernie zapełni linię pamięci podręcznej.
TL; DR: wyrównanie jest ważne.