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:
Niewłaściwy dostęp może być poważnym błędem (często SIGBUS).
Niewłaściwie wyrównany dostęp może być miękkim błędem.
Albo poprawione sprzętowo, aby uzyskać niewielki spadek wydajności.
Lub poprawione przez emulację w oprogramowaniu w celu poważnego obniżenia wydajności.
Ponadto atomowość i inne gwarancje współbieżności mogą zostać zerwane, co prowadzi do subtelnych błędów.
Oto przykład z typowymi ustawieniami procesora x86 (wszystkie używane tryby 32- i 64-bitowe):
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 Zw 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 #pragmainstrukcje do zmiany ustawień wyrównania struktury.
Chcę tutaj zanotować: większość procesorów karze cię za niewyrównany dostęp do pamięci (jak wspomniałeś), ale nie możesz zapomnieć, że wielu całkowicie go nie zezwala. W szczególności większość żetonów MIPS spowoduje wyjątek dla nieprzypisanego dostępu.
Cody Brocious,
35
Układy x86 są w rzeczywistości raczej unikalne, ponieważ umożliwiają niezaangażowany dostęp, aczkolwiek karany; AFAIK większość żetonów wyrzuci wyjątki, nie tylko kilka. PowerPC jest kolejnym częstym przykładem.
Dark Shikari,
6
Włączenie pragmatyki dla niezrównanego dostępu ogólnie powoduje, że twój kod się powiększa, na procesorach, które generują błędy niewspółosiowości, ponieważ należy wygenerować kod, który naprawi każdą niewspółosiowość. ARM generuje również błędy niewspółosiowości.
Mike Dimmick
5
@Dark - całkowicie się zgadzam. Ale większość procesorów do komputerów stacjonarnych to x86 / x64, więc większość układów nie powoduje błędów wyrównywania danych;)
Aaron
27
Nieprzypisany dostęp do danych jest zwykle funkcją występującą w architekturach CISC, a większość architektur RISC go nie obejmuje (ARM, MIPS, PowerPC, Cell). W rzeczywistości większość układów NIE jest procesorami do komputerów stacjonarnych, ponieważ są one osadzone według liczby układów, a zdecydowana większość z nich to architektury RISC.
Lara Dougan,
192
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.
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 |+-------+-------+-------+-------+
@EmmEff może to być źle, ale nie całkiem rozumiem: dlaczego nie ma gniazda pamięci dla wskaźnika w tablicach?
Balázs Börcsök
1
@ BalázsBörcsök Są to tablice o stałej wielkości, więc ich elementy są przechowywane bezpośrednio w strukturze o ustalonych przesunięciach. Kompilator wie o tym wszystkim w czasie kompilacji, więc wskaźnik jest niejawny. Na przykład, jeśli masz zmienną struct tego typu o nazwie swtedy &s.a == &si &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. aZostał zadeklarowany char a[]zamiast char a[3]), ale wtedy elementy muszą być przechowywane gdzie indziej.
kbolino
27
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.
„dobre powody” Przykład: Utrzymywanie zgodności binarnej (wypełniania) między systemami 32-bitowymi i 64-bitowymi w celu uzyskania złożonej struktury w kodzie demonstracyjnym typu proof-of-concept, który zostanie zaprezentowany jutro. Czasami konieczność musi mieć pierwszeństwo przed właściwością.
Mr.Ree,
2
Wszystko jest w porządku, chyba że wspominasz o systemie operacyjnym. Jest to problem związany z szybkością procesora, system operacyjny w ogóle nie jest zaangażowany.
Blaisorblade,
3
Innym dobrym powodem jest umieszczanie strumienia danych w strukturze, np. Podczas analizowania protokołów sieciowych.
ceo
1
@dolmen Właśnie zauważyłem, że „systemowi Operatin łatwiej jest uzyskać dostęp do danych” jest nieprawidłowy, ponieważ system operacyjny nie ma dostępu do danych.
Blaisorblade
1
@dolmen W rzeczywistości należy mówić o ABI (binarnym interfejsie aplikacji). Domyślne wyrównanie (używane, jeśli nie zmienisz go w źródle) zależy od ABI, a wiele systemów operacyjnych obsługuje wiele ABI (powiedzmy 32- i 64-bitowy lub dla plików binarnych z różnych systemów operacyjnych lub dla różnych sposobów kompilacji te same pliki binarne dla tego samego systemu operacyjnego). OTOH, to, jakie wyrównanie jest wygodne pod względem wydajności, zależy od procesora - pamięć jest dostępna w ten sam sposób, niezależnie od tego, czy używasz trybu 32- lub 64-bitowego (nie mogę komentować trybu rzeczywistego, ale w dzisiejszych czasach wydaje się mało istotny dla wydajności). IIRC Pentium zaczął preferować wyrównanie do 8 bajtów.
Blaisorblade
15
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:
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).
„C w systemie Linux za pomocą gcc” nie wystarcza do opisania twojej platformy. Wyrównanie zależy głównie od architektury procesora.
dolmen
- @ Kyle Burton. Przepraszam, nie rozumiem, dlaczego rozmiar struktury „someBits” jest równy 4, oczekuję 8 bajtów, ponieważ zadeklarowano 2 liczby całkowite (2 * sizeof (int)) = 8 bajtów. dzięki
youpilat13
1
Cześć @ youpilat13, :2i :6faktycznie 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
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.
„Jestem pewien, że kolejność członków jest mruknięta w C”. Tak, C99 mówi: „W obrębie obiektu struktury elementy niebędące polami bitowymi i jednostki, w których rezydują pola bitowe, mają adresy, które zwiększają się w kolejności, w której są deklarowane”. Bardziej standardowa dobroć na: stackoverflow.com/a/37032302/895245
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 1structMyStruct{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.
Omawia to konsekwencje wypełnienia konstrukcji, ale nie odpowiada na pytanie.
Keith Thompson,
„ ... z powodu tego, co nazywa się pakowaniem. ... - Myślę, że masz na myśli„ wypełnianie ”.„ Preferowany rozmiar większości nowoczesnych procesorów, jeśli 32-bitowy (4 bajty) ”- To trochę nadmierne uproszczenie. Zazwyczaj rozmiarach 8, 16, obsługiwane są 32 i 64 bity, często każdy rozmiar ma swoje własne ustawienie i nie jestem pewien, odpowiedź dodaje wszelkie nowe informacje, które nie znajduje się już w przyjętym odpowiedź..
Keith Thompson
1
Kiedy mówiłem „pakowanie”, miałem na myśli sposób, w jaki kompilator pakuje dane do struktury (i może to zrobić, wypełniając małe elementy, ale nie musi wypełniać, ale zawsze się pakuje). Jeśli chodzi o rozmiar - mówiłem o architekturze systemu, a nie o tym, co system będzie obsługiwał dostęp do danych (co różni się znacznie od podstawowej architektury magistrali). Jeśli chodzi o twój ostatni komentarz, podałem uproszczone i rozszerzone wyjaśnienie jednego aspektu kompromisu (szybkość w porównaniu z rozmiarem) - głównego problemu programistycznego. Opisuję również sposób rozwiązania problemu - tego nie było w zaakceptowanej odpowiedzi.
sid1138
„Pakowanie” w tym kontekście zwykle odnosi się do przydzielania członków bardziej niż domyślnie, jak w przypadku #pragma pack. Jeśli członkowie zostaną przydzieleni w ramach domyślnego wyrównania, ogólnie powiedziałbym, że struktura nie jest zapakowana.
Keith Thompson,
Pakowanie to rodzaj przeciążonego terminu. Oznacza to, w jaki sposób umieszczasz elementy struktury w pamięci. Podobne do znaczenia wkładania przedmiotów do pudełka (pakowanie do przenoszenia). Oznacza to także zapisywanie elementów w pamięci bez wyściółki (rodzaj krótkiej ręki dla „ciasno upakowanego”). Następnie jest wersja polecenia tego słowa w poleceniu #pragma pack.
sid1138
5
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.
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.
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)
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.
Nie do końca. W typowych implementacjach do struktury dodaje się wskaźnik vtable .
Don Wakefield
3
Język C pozostawia kompilatorowi pewną swobodę w zakresie lokalizacji elementów strukturalnych w pamięci:
dziury pamięci mogą pojawić się między dowolnymi dwoma komponentami i po ostatnim komponencie. Wynika to z faktu, że niektóre typy obiektów na komputerze docelowym mogą być ograniczone przez granice adresowania
Rozmiar „otworów pamięci” zawarty w wyniku sizeof operatora. Sizeof nie obejmuje tylko rozmiaru elastycznej tablicy, która jest dostępna w C / C ++
Niektóre implementacje języka pozwalają kontrolować układ struktur za pomocą opcji pragma i kompilatora
Język C zapewnia programistom pewne zapewnienie układu elementów w strukturze:
kompilatory wymagane do przypisania sekwencji komponentów zwiększających adresy pamięci
Adres pierwszego komponentu pokrywa się z adresem początkowym struktury
nienazwane pola bitowe mogą być zawarte w strukturze do wymaganego wyrównania adresu sąsiednich elementów
Problemy związane z wyrównaniem elementów:
Różne komputery na różne sposoby ustawiają krawędzie obiektów
Różne ograniczenia szerokości pola bitowego
Komputery różnią się sposobem przechowywania bajtów jednym słowem (Intel 80x86 i Motorola 68000)
Jak działa wyrównanie:
Objętość zajmowana przez strukturę jest obliczana jako rozmiar wyrównanego pojedynczego elementu tablicy takich struktur. Struktura powinna zakończyć się, aby pierwszy element następnej następnej struktury nie naruszał wymagań wyrównania
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 {unsignedchar red;// 0unsignedchar green;// 1unsignedint alpha;// 4 (gotta skip to an aligned offset)unsignedchar 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.
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):
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.źródło
Pakowanie i wyrównanie bajtów, zgodnie z opisem w C FAQ tutaj :
źródło
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ł zadeklarowanychar a[]
zamiastchar 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ć.
źródło
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:
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).
źródło
: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-cZobacz 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.
źródło
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ę:
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:
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.
źródło
#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.
źródło
Wersja standardowa C99 N1256
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 Wielkość operatora :
6.7.2.1 Specyfikacje struktury i związków :
i:
Nowa funkcja elastycznego elementu tablicy C99 (
struct S {int is[];};
) może również wpływać na dopełnianie:Załącznik J Zagadnienia dotyczące przenośności przypomina:
Wersja standardowa C ++ 11 N3337
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Rozmiar :
9.2 Członkowie klasy :
Znam wystarczająco dużo C ++, aby zrozumieć notatkę :-)
źródło
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.
źródło
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)”
źródło
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.
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.
źródło