A struktúra tagjainak logikai sorrendje nem jelenti azt, hogy a tagok hézagmentesen követik egymást a memóriában. A fordító az igazítási követelmények miatt kitöltést (padding) helyezhet közéjük és az objektum végére. Ez befolyásolja a memóriaigényt, a tömbök lépésközét és a bináris interfészek kompatibilitását, de nem része a struktúrával modellezett logikai értéknek.
A Struktúrák, tagok és értékmásolás rész értékszemantikájára, valamint az Alignment és speciális allokációk igazításfogalmára építve most egy konkrét elrendezést vezetünk le. Ezután olyan mérőprogramot készítünk, amely összehasonlítja a levezetést a helyi fordító eredményével. A közölt számok levezetett várakozások, nem futtatással kapott eredmények.
ISO C17. Egy közönséges, nem bitmező tagokat
tartalmazó struktúrában a tagok címei deklarációs sorrendben növekednek.
Az első tag előtt nincs padding, a tagok között és az utolsó tag után
azonban lehet. A struktúra sizeof értéke a teljes objektum
mérete, a kitöltéssel együtt.
Kétféle kitöltést különböztetünk meg:
A méret és az igazítás mértékegysége a C bájt, azaz
C_byte. Egy ilyen bájt bitszáma CHAR_BIT;
nem minden C implementációban nyolc.
A sizeof(T) a típus objektumainak méretét, az
_Alignof(T) az igazítási követelményét adja meg. A
<stddef.h> fejléc offsetof(T, member)
makrója a tag kezdetének eltolását szolgáltatja a struktúra kezdetétől,
ugyancsak C bájtban. Mindhárom eredmény size_t típusú.
Az offsetof nem egy konkrét példány címét méri: a típus
elrendezésének tulajdonságát kérdezi le. Bitmezőre nem használható. Nem
érdemes null pointerből képzett tagcímekkel „házilag”
helyettesíteni.
ABI-laborfeltevés. A következő levezetés profilja Linux, x86-64 System V ABI, LP64 adatmodell, nyolcbites C bájt, természetes tagigazítás, csomagolási kapcsolók és attribútumok nélkül. A választott fordítók GCC 14.2.0 és Clang 19.1.7, a libc glibc 2.40; a helyi verziókat és a tényleges párosítást külön rögzíteni kell.
Ebben a profilban a példánkhoz szükséges típusjellemzők:
| Típus | Méret, C_byte | Igazítás, C_byte |
|---|---|---|
unsigned char |
1 | 1 |
uint16_t |
2 | 2 |
uint32_t |
4 | 4 |
Az exact-width típusok általánosan nem kötelezően elérhetők. A mérőprogram ezért ellenőrzi jelenlétüket. A táblázat ABI-feltevéseit a program helyben ellenőrzi; itt nem áll rendelkezésre külön ellenőrzött ABI-forrásjegyzet.
Az alábbi C17-részlet lesz a mérés alapja; a teljes programban is szerepel:
struct Record {
unsigned char kind;
uint32_t count;
uint16_t code;
};Legyen a következő szabad eltolás, pedig a következő tag igazítása. A legkisebb megfelelő eltolás:
Maradékos osztással ugyanez:
Bizonyításvázlat. Ha , nincs szükség kitöltésre. Egyébként, ha a maradék , akkor pontosan bájt hozzáadásával érjük el a következő -val osztható eltolást. Ennél kisebb nemnegatív növekmény nem ad megfelelő igazítást.
Ez matematikai képlet. C-kódban a gyakori
(x + a - 1) / a * a alak túlcsordulhat. A maradékból
számított padding hozzáadásakor is ellenőrizni kell, hogy az összeg
ábrázolható-e.
A választott ABI közönséges struktúráinál a levezetési modell:
Itt a struktúra igazítása a tagigazítások maximuma:
Ez a konkrét profil szabálya, nem minden C implementációra érvényes „mindig a minimumot választja” garancia.
A kind az első tag, ezért eltolása nulla, vége pedig
.
A count négybájtos igazítást igényel:
A következő tag tehát nem az 1., hanem a 4. bájton kezdődik. Közéjük
három bájt padding kerül. A count vége
.
A code kétbájtos igazítása már teljesül:
Vége . A teljes méretet még négy többszörösére kerekítjük:
| Tartomány, C_byte | Tartalom | Hossz |
|---|---|---|
kind |
1 | |
| belső padding | 3 | |
count |
4 | |
code |
2 | |
| záró padding | 2 |
A tagok méretének összege , de a struktúráé 12 C bájt.
ISO C17. A tömbelemek folytonosan helyezkednek el;
az egymást követő elemek közötti lépésköz az elemtípus
sizeof értéke. Nincs ezen felüli, külön tömbelemközi
rés.
Ha a struktúra mérete 10 lenne, a második elem kezdete az elsőhöz képest 10 bájttal tolódna el. Ez nem osztható néggyel, így nem biztosítaná a szükséges igazítást.
A 12 bájtos mérettel:
ahol a megfelelően igazított tömbkezdet. Mivel osztható néggyel, minden elem megfelelő helyre kerül.
| Elem | Kezdet | count kezdete |
Objektum vége |
|---|---|---|---|
records[0] |
0 | 4 | 12 |
records[1] |
12 | 16 | 24 |
records[2] |
24 | 28 | 36 |
Az eltolások a tömb kezdetéhez viszonyított értékek, nem univerzális memóriacímek.
A következő önálló program ISO C17-et és a megadott laborprofilt célozza. Nem olvassa ki a padding tartalmát: méreteket, igazításokat és eltolásokat vizsgál.
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#if CHAR_BIT != 8
#error "This lab requires 8-bit C bytes."
#endif
#if !defined(UINT16_MAX) || !defined(UINT32_MAX)
#error "This lab requires uint16_t and uint32_t."
#endif
struct Record {
unsigned char kind;
uint32_t count;
uint16_t code;
};
static int check(const char *name, size_t actual, size_t expected)
{
int same = actual == expected;
printf("%-24s actual=%zu expected=%zu %s\n",
name, actual, expected, same ? "OK" : "DIFF");
return same;
}
int main(void)
{
struct Record records[2];
int ok = 1;
ok &= check("sizeof(uint16_t)", sizeof(uint16_t), 2);
ok &= check("alignof(uint16_t)", _Alignof(uint16_t), 2);
ok &= check("sizeof(uint32_t)", sizeof(uint32_t), 4);
ok &= check("alignof(uint32_t)", _Alignof(uint32_t), 4);
ok &= check("Record.kind", offsetof(struct Record, kind), 0);
ok &= check("Record.count", offsetof(struct Record, count), 4);
ok &= check("Record.code", offsetof(struct Record, code), 8);
ok &= check("alignof(Record)", _Alignof(struct Record), 4);
ok &= check("sizeof(Record)", sizeof(struct Record), 12);
ok &= check("sizeof(records)", sizeof records, 24);
return ok ? 0 : 1;
}A check szöveges neveket kölcsönöz statikus tárolási
idejű sztringliterálokból; nem tárolja el és nem módosítja őket. Nincs
felszabadítási feladata vagy futás közbeni invalidáló művelete. A
records automatikus objektum, de inicializálatlansága itt
nem probléma: a sizeof nem olvassa az elemek értékét.
Fordítás a külön tanulói laborban, a megfelelő verziókra beállított parancsnevekkel:
gcc -std=c17 -O2 -Wall -Wextra -Wconversion -pedantic-errors \
layout.c -o layout-gcc
clang -std=c17 -O2 -Wall -Wextra -Wconversion -pedantic-errors \
layout.c -o layout-clangA program kimenetében az actual oszlop lesz a helyi
megfigyelés, az expected a levezetett érték. A várt
visszatérési kód nulla. Az egyes nem nulla kód
profilkülönbséget jelez, nem önmagában hibás C
implementációt.
Eltéréskor először a targetet, a fordítóverziót és a tényleges kapcsolókat vizsgáld. Különösen fontosak a csomagolási beállítások és a korábban hatályba lépett pragmák. Az egyezés sem bizonyít általános hordozhatóságot: csak ezt a típust és ezt a konfigurációt ellenőrzi.
Ugyanebben a profilban az alábbi, önállóan nem teljes C17-részlet kevesebb kitöltést igényel:
struct CompactRecord {
uint32_t count;
uint16_t code;
unsigned char kind;
};| Tulajdonság | Record |
CompactRecord |
|---|---|---|
count eltolása |
4 | 0 |
code eltolása |
8 | 4 |
kind eltolása |
0 | 6 |
| Tagok utáni végpont | 10 | 7 |
| Igazítás | 4 | 4 |
| Teljes méret | 12 | 8 |
| Padding összesen | 5 | 1 |
A második változatnál a végső kerekítés . Egy elemű tömb tárolóigénye így helyett C bájt, vagyis egyharmaddal kisebb. Ez tárolóméret-számítás, nem gyorsulási mérés.
A csökkenő igazítás szerinti rendezés gyakran hasznos heurisztika, de nem általános optimalizálási bizonyítás.
ABI-következmény. A két struktúra nem binárisan felcserélhető. Megváltoznak a tageltolások, a méret és a tömblépésköz. Egy korábban lefordított kliens továbbra is a régi helyeken keresné az adatokat. Az azonos tagnevek és azonos logikai jelentés nem mentik meg az ABI-kompatibilitást.
Publikus struktúra átrendezését ezért interfészváltoztatásként kell kezelni. Az opaque handle-alapú határ lehetőségeit az Opaque handle és stabil C könyvtári interfész tárgyalja.
ISO C17. A padding nem tagérték. Tartalmára nem szabad alkalmazásszintű jelentést építeni; struktúrába vagy tagjába történő értéktárolás után a padding bájtjai nem specifikált értékeket vehetnek fel.
Ez pontosabb megfogalmazás, mint pusztán azt mondani, hogy „inicializálatlanok”: az inicializált tagokból sem következik stabil, kanonikus teljes bájtreprezentáció.
Két struktúra logikailag egyenlő lehet, miközben a paddingjük eltér.
Emiatt a teljes objektumon végzett memcmp nem megfelelő
általános értékegyenlőség-vizsgálat.
A helyes összehasonlítás ennél a típusnál, C17-részletként:
static int record_equal(struct Record a, struct Record b)
{
return a.kind == b.kind
&& a.count == b.count
&& a.code == b.code;
}Előfeltétel, hogy mindkét argumentum minden tagja inicializált, érvényes értéket tartalmazzon. Paddinget a függvény nem vizsgál.
Fontos különbségek:
memcpy objektumreprezentációt másol, a paddinggel
együtt.memset nem általános garancia arra, hogy
későbbi értékadások után is nullák maradnak a kitöltő bájtok.memcmp hibás szemantikai választásából nem
következik, hogy minden ilyen hívás automatikusan UB.A teljes struktúratároló naplózása vagy külső félnek átadása korábbi memóriatartalom részleteit is felfedheti a paddingen keresztül. Ez információszivárgási kockázat, nem csupán hordozhatósági probléma. Külső adatformátumhoz explicit mezőkódolás kell; ezt a Byte order és biztonságos szerializálás fejti ki.
GCC/Clang-bővítés, nem ISO C17. Az alábbi különálló tanítási részlet csomagolt elrendezést kér:
struct __attribute__((packed)) PackedRecord {
unsigned char kind;
uint32_t count;
uint16_t code;
};A megadott x86-64 laborprofilban a várt eltolások 0, 1 és 5; a várt méret 7, az igazítás 1. Ezeket külön fordítói próbával kell ellenőrizni.
A count így nincs feltétlenül a saját típusának
természetes igazításán. A fordító a packed tag közvetlen elérésekor
ismeri ezt, és ennek megfelelő kódot állíthat elő. A tag címének
közönséges uint32_t * pointerként való továbbadása viszont
veszélyes: a fogadó kód már megfelelő igazítású objektumot
feltételezhet. Ha ez nem teljesül, a dereferálás UB lehet. Az, hogy a
hardver bizonyos igazítatlan utasításokat végrehajt, nem írja felül a C
szabályait.
A #pragma pack szintén implementációspecifikus. Hatása
további deklarációkra is átterjedhet; a mentési-visszaállítási fegyelem
hiánya különösen veszélyes headerekben.
A packed elrendezés:
A típusigazítás és a cache line mérete külön fogalom. A négyszeres címre igazított objektum nem feltétlenül cache line-határra igazított.
Egy külön hardverfeltevésként 64 bájtos cache line-t használó gépen több kis struktúra kerülhet ugyanabba a vonalba. Ha külön szálak külön objektumokat módosítanak, de azok ugyanazon cache line-on osztoznak, a koherenciaforgalom teljesítményveszteséget okozhat. Ez a false sharing.
A jelenség helyesen szinkronizált programban is előfordulhat; nem azonos a nyelvi adatversennyel. A padding csökkentése ezért javíthatja a lokalitást, de ronthatja a szálak közötti elkülönítést. A részletes költségmodell a Cache, locality, alignment és false sharing témája.
A laborprofilban mekkora egy struct Record records[5]
tömb? Mennyi helyet foglalnak a tagok összesen, és mennyi a padding?
Miért nincs külön rés a tömbelemek között?
Egészítsd ki a mérőprogramot a CompactRecord
vizsgálatával. Ellenőrizd minden tag eltolását, az igazítást és a
méretet. Készíts hozzá tagonkénti egyenlőségvizsgálatot is.
Egy szolgáltatás teljes struktúrákat ír naplófájlba, és
memcmp segítségével dönti el, változott-e egy rekord. A
paddingproblémát packed attribútummal javítanák. Mely hibák maradnak
meg, és milyen új kockázat jelenik meg?
A teljes méret:
A tagok méretének összege , a paddingé C bájt. A záró padding már minden elem 12 bájtos méretében szerepel; ezen felül nincs külön tömbelemközi rés.
Ellenőrzési feltétel: a helyi profilban
sizeof records == 60.
A várt eltolások: count: 0, code: 4,
kind: 6. Az igazítás 4, a méret 8.
Az egyenlőségvizsgálat ugyanazt a három tagértéket hasonlítsa össze,
mint a record_equal, csak struct CompactRecord
paraméterekkel.
Ellenőrzési feltétel: mindkét fordító minden
layout-ellenőrzése egyezést jelezzen. Az egyenlőségteszt azonos
tagértékekre legyen igaz, bármely egyetlen tag megváltoztatására hamis.
Teljes struktúrára végzett memcmp ne szerepeljen benne.
A padding eltüntetése nem definiál tartós, verziózható külső formátumot, és nem kezeli a bájtsorrendet. Más tagtípusoknál az eltérő értékreprezentációk miatt a bájtszintű összehasonlítás padding nélkül sem általános értékegyenlőség.
Új kockázat az igazítatlan tagelérés és különösen a tagcím közönséges típusos pointerként való továbbadása. A javítás a tagonkénti összehasonlítás és a külön meghatározott külső kódolás.
Ellenőrzési feltétel: a naplóformátum ne függjön
sizeof(struct ...) értékétől vagy tageltolásoktól; az
összehasonlítás kizárólag logikai tagértékeket vizsgáljon.
A struktúra méretét a tagméretek mellett az igazítás és a padding
határozza meg. A záró padding biztosítja, hogy tömbben a következő elem
is megfelelően igazított legyen. Az offsetof,
_Alignof és sizeof a helyi elrendezés
ellenőrzésének eszközei.
A tagátrendezés csökkentheti a memóriaigényt, de ABI-változás. A logikai érték nem azonos a teljes objektumreprezentációval; a padding nem megbízható összehasonlítási vagy adatátviteli alap, a packed elrendezés pedig nem általános megoldás.