9.2 Padding, alignment és offsetof

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.

9.2.1 A nyelvi garancia és az ABI választása

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.

9.2.2 Az elrendezés levezetése

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;
};

9.2.2.1 Igazítási kerekítés

Legyen xx a következő szabad eltolás, a>0a>0 pedig a következő tag igazítása. A legkisebb megfelelő eltolás:

round_up(x,a)=a⌈xa⌉. \operatorname{round\_up}(x,a) = a\left\lceil \frac{x}{a}\right\rceil.

Maradékos osztással ugyanez:

round_up(x,a)=x+((a−(xmoda))moda). \operatorname{round\_up}(x,a) = x+\bigl((a-(x\bmod a))\bmod a\bigr).

Bizonyításvázlat. Ha xmoda=0x\bmod a=0, nincs szükség kitöltésre. Egyébként, ha a maradék rr, akkor pontosan a−ra-r bájt hozzáadásával érjük el a következő aa-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:

  1. A következő tag kezdetét felkerekítjük a tag igazítására.
  2. Hozzáadjuk a tag méretét.
  3. Az utolsó tag végét felkerekítjük a struktúra igazítására.

Itt a struktúra igazítása a tagigazítások maximuma:

A=max(1,4,2)=4. A=\max(1,4,2)=4.

Ez a konkrét profil szabálya, nem minden C implementációra érvényes „mindig a minimumot választja” garancia.

9.2.2.2 Belső és záró kitöltés

A kind az első tag, ezért eltolása nulla, vége pedig 0+1=10+1=1.

A count négybájtos igazítást igényel:

round_up(1,4)=4. \operatorname{round\_up}(1,4)=4.

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 4+4=84+4=8.

A code kétbájtos igazítása már teljesül:

round_up(8,2)=8. \operatorname{round\_up}(8,2)=8.

Vége 8+2=108+2=10. A teljes méretet még négy többszörösére kerekítjük:

S=round_up(10,4)=12. S=\operatorname{round\_up}(10,4)=12.

Tartomány, C_byte Tartalom Hossz
[0,1)[0,1) kind 1
[1,4)[1,4) belső padding 3
[4,8)[4,8) count 4
[8,10)[8,10) code 2
[10,12)[10,12) záró padding 2

A tagok méretének összege 1+4+2=71+4+2=7, de a struktúráé 12 C bájt.

9.2.2.3 Miért kell a záró padding?

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:

elemkezdés(k)=B+12k, \text{elemkezdés}(k)=B+12k,

ahol BB a megfelelően igazított tömbkezdet. Mivel 12k12k 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.

9.2.3 A levezetés összevetése a helyi eredménnyel

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-clang

A 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.

9.2.4 Tagátrendezés: kisebb objektum, más ABI

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 round_up(7,4)=8\operatorname{round\_up}(7,4)=8. Egy NN elemű tömb tárolóigénye így 12N12N helyett 8N8N 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.

9.2.5 Logikai érték és teljes objektumreprezentáció

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:

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.

9.2.6 Packed elrendezés és igazítatlan hozzáférés

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:

9.2.7 Cache line és a false sharing előképe

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.

9.2.8 Feladatok

9.2.8.1 Magyarázó feladat: tömbméret

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?

9.2.8.2 Kódolási feladat: átrendezés

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.

9.2.8.3 Diagnosztikai feladat: „javítás” packed típussal

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?

9.2.9 Megoldások

9.2.9.1 Tömbméret

A teljes méret:

5⋅12=60C_byte. 5\cdot12=60\ \text{C\_byte}.

A tagok méretének összege 5⋅7=355\cdot7=35, a paddingé 5⋅5=255\cdot5=25 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.

9.2.9.2 Átrendezés ellenőrzése

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.

9.2.9.3 A packed „javítás” értékelése

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.

9.2.10 Összefoglaló

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.