8.3 Alignment és speciális allokációk

A dinamikusan lefoglalt tároló mérete önmagában nem határozza meg, hogy milyen objektum helyezhető el benne. A kezdőcímnek az objektum igazítási követelményét (alignment requirement) is teljesítenie kell. Egy megfelelően nagy, de rosszul igazított tároló nem válik alkalmassá egy típus befogadására attól, hogy a címét a megfelelő pointertípusra konvertáljuk.

A malloc, calloc, free és allokációs szerződés alapján már ismerjük a dinamikus tároló élettartamát és felszabadítását. Most ehhez hozzáadjuk az igazítás szerződését. A Deklarációk, typedef és típusminősítők fogalmaira építve külön kezeljük a típus követelményét, az egyedi objektum deklarációját és az allocator vállalását.

8.3.1 A típus és az objektum igazítása

ISO C17. Az igazítás azt korlátozza, hogy egy objektum milyen címeken helyezkedhet el. Mértékegysége a C bájt, vagyis a sizeof egysége; ez nem általánosan azonos a nyolcbites oktettel.

Egy teljes objektumtípus igazítását a _Alignof(T) operátor adja meg. Eredménye size_t típusú egész konstanskifejezés. Például _Alignof(double) a double típus igazítását jelenti, nem egy kiválasztott double objektum címéből megfigyelt tulajdonságot.

A _Alignas deklarációban ír elő igazítást. Argumentuma lehet támogatott igazítási érték vagy típusnév; utóbbi esetben az adott típus igazítása az előírás. Nem gyengítheti az objektum egyébként szükséges igazítását. A nulla értékű igazítási előírásnak nincs hatása.

Az alábbi C17 deklarációrészlet olyan compiler/target profilt feltételez, amely ebben a kontextusban támogatja a 64 C bájtos igazítást:

_Alignas(64) unsigned char workspace[1000];

struct simd_block {
    _Alignas(64) unsigned char bytes[64];
};

A két eset közötti különbség fontos:

A tömb kezdetének igazítása nem jelenti minden elem ugyanilyen igazítását. A workspace[1] egy C bájttal követi a kezdetet. Ez az unsigned char számára megfelelő, de nem 64-es igazítású cím.

8.3.1.1 Fundamental és extended alignment

ISO C17. A fundamental alignment, vagy alapvető igazítás, legfeljebb _Alignof(max_align_t) értékű igazítás. A max_align_t típust a <stddef.h> deklarálja. Az ennél nagyobb igazítás extended alignment, azaz kiterjesztett igazítás; az ilyen követelményű típus túligazított (over-aligned).

A támogatott kiterjesztett igazítások és használati kontextusaik implementáció által meghatározottak. A megengedett igazítási értékek kettőhatványok, de ebből nem következik, hogy minden kettőhatvány minden kontextusban támogatott.

A max_align_t tehát nem a gépen elképzelhető legnagyobb igazítást jelenti. Különösen nem ígéri, hogy a közönséges allokáció automatikusan teljesít minden SIMD-utasítás vagy platformbővítmény követelményét.

8.3.2 Mit vállalnak az allocator API-k?

Az alábbi táblázatban pozitív méretű kéréseket feltételezünk. Könyvtári API-ban célszerű a nulla elemszámot külön, például null pointerrel reprezentált üres állapotként kezelni.

API és réteg A sikeres kérés igazítási és méretfeltétele Hibajelzés Élettartamot lezáró művelet
malloc, ISO C17 Elegendő méret esetén alkalmas alapvető igazítású objektumhoz; tetszőleges kiterjesztett igazítást nem ígér Null pointer free
aligned_alloc, ISO C17 Az igazítás érvényes és támogatott; a méret az igazítás egész számú többszöröse Null pointer free
posix_memalign, POSIX Az igazítás kettőhatvány és sizeof(void *) többszöröse; a méretnek nem kell az igazítás többszörösének lennie Közvetlenül visszaadott hibakód free
mmap, POSIX/Linux Leképezési, nem C-típushoz szabott szerződés; megfelelő használat mellett lapigazított kezdőcím MAP_FAILED munmap

Mindegyik sikeres allokációhoz vagy leképezéshez ki kell jelölni egy tulajdonost. A belőle képzett pointerek csak kölcsönzött hozzáférések: a tároló felszabadítása vagy leválasztása mindet érvényteleníti.

8.3.2.1 malloc és SIMD

ISO C17. A malloc elegendő méretű tárolója használható alapvető igazítású típusokhoz. Ebből nem vezethető le egy önkényesen választott 32 vagy 64 C bájtos igazítás.

Platformfüggő SIMD. Az utasításkészlet, az intrinsic és a fordítóbővítmény szerződése dönt arról, hogy szükséges-e erősebb igazítás. Egyes műveletek igazítatlan hozzáférést is támogatnak; mások igazítási előfeltételt támasztanak. A túligazítás önmagában sem gyorsulást, sem cache-line-elkülönítést nem garantál.

Az igazítás csak egy helyességi feltétel. A megfelelő méret, élettartam, inicializálás és effektív típus szabályai változatlanul érvényesek.

8.3.2.2 aligned_alloc

ISO C17. A <stdlib.h> által deklarált függvény az igazítást és a lefoglalandó méretet külön kapja meg:

/* Deklaráció emlékeztetőként; a tényleges deklarációt a header adja. */
void *aligned_alloc(size_t alignment, size_t size);

A fejezetben használt szerződés szerint a hívó előzetesen biztosítja:

  1. az igazítás az implementáció által támogatott érvényes érték;
  2. a méret az igazítás többszöröse;
  3. a méretszámítás nem csordult túl.

Az allocator nem inicializálja az adatokat. A null pointert ellenőrizni kell; ISO C alapján nem építünk arra, hogy az errno meghatározott hibakódot tartalmaz.

8.3.2.3 posix_memalign

POSIX, nem ISO C. A függvény egy kimeneti paraméterbe írja a kapott pointert:

/* POSIX deklaráció emlékeztetőként. */
int posix_memalign(void **memptr, size_t alignment, size_t size);

Az igazítás kettőhatvány és sizeof(void *) többszöröse legyen. A méret lehet például 1000 akkor is, ha az igazítás 64.

Siker esetén a visszatérés 0. Hibás igazításra EINVAL, elégtelen memóriára ENOMEM hibakódot ad vissza. A visszatérési értéket kell vizsgálni, nem az errno változót. A jelenlegi POSIX-szerződés szerint hibánál a kimeneti pointer változatlan marad.

A kimeneti argumentumhoz valódi void * változó címét használjuk. Egy T ** pointer void ** típusra castolása nem általános megoldás: a többszintű pointerkonverzió nem követi a void * és objektumpointer közötti egyszerű konverzió szabályát.

8.3.3 Méretkerekítés túlcsordulás nélkül

Legyen n>0n>0 a hasznos méret, a>0a>0 az igazítás, mindkettő C bájtban. Az aligned_alloc számára szükséges legkisebb megfelelő méret:

r=⌈na⌉a. r=\left\lceil\frac{n}{a}\right\rceil a.

A közvetlen (n + a - 1) / a * a kifejezésben az összeadás túlcsordulhat. Biztonságosabb először a maradékot számítani:

m=nmoda. m=n\bmod a.

Ha m=0m=0, akkor r=nr=n. Egyébként a hozzáadandó kitöltés:

d=a−m,r=n+d. d=a-m, \qquad r=n+d.

Az összeadás pontosan akkor ábrázolható size_t típusban, ha

n≤SIZE_MAX−d. n\leq \mathrm{SIZE\_MAX}-d.

Bizonyításvázlat. Ha m≠0m\neq0, akkor 0<m<a0<m<a, így 0<d<a0<d<a. Az nn számhoz éppen a következő aa-többszörösig hiányzó mennyiséget adjuk. A kivonásos ellenőrzés még az összeadás előtt kizárja a túlcsordulást.

A következő adatok matematikai méretszámítások, nem mért allocator-layoutok:

Hasznos méret nn Igazítás aa Lefoglalt méret rr Többlet
1000 64 1024 24
1024 64 1024 0
1025 64 1088 63

8.3.3.1 Teljes példa két allokációs API-val

A választott, helyben ellenőrizendő laborprofil: Linux x86-64 System V ABI, LP64, CHAR_BIT == 8, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program C17-et és a POSIX posix_memalign függvényét használja; a profilban 64 C bájtos igazítást kérünk.

A fordítási parancsok elvégzendő ellenőrzések, nem dokumentált teszteredmények.

/* alignment_demo.c */
#define _POSIX_C_SOURCE 200112L

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

/* A 0 visszatérés érvénytelen bemenetet vagy túlcsordulást jelez. */
static size_t rounded_size(size_t n, size_t alignment)
{
    if (n == 0 || alignment == 0) {
        return 0;
    }

    size_t remainder = n % alignment;
    if (remainder == 0) {
        return n;
    }

    size_t padding = alignment - remainder;
    if (n > SIZE_MAX - padding) {
        return 0;
    }

    return n + padding;
}

int main(void)
{
    const size_t alignment = 64;
    const size_t payload = 1000;
    const size_t reserved = rounded_size(payload, alignment);

    if (reserved == 0) {
        fputs("Invalid allocation size\n", stderr);
        return EXIT_FAILURE;
    }

    void *owner = aligned_alloc(alignment, reserved);
    if (owner == NULL) {
        fputs("aligned_alloc failed\n", stderr);
        return EXIT_FAILURE;
    }

    /* Borrowed view: csak az owner felszabadításáig érvényes. */
    unsigned char *view = owner;
    for (size_t i = 0; i < payload; ++i) {
        view[i] = 0;
    }

    printf("fundamental=%zu requested=%zu reserved=%zu\n",
           _Alignof(max_align_t), alignment, reserved);

    free(owner);
    owner = NULL;
    view = NULL;

    /* POSIX: a méretet itt nem kell 64 többszörösére kerekíteni. */
    void *posix_owner = NULL;
    int rc = posix_memalign(&posix_owner, alignment, payload);
    if (rc != 0) {
        fprintf(stderr, "posix_memalign error: %d\n", rc);
        return EXIT_FAILURE;
    }

    free(posix_owner);
    posix_owner = NULL;
    return EXIT_SUCCESS;
}

Fordítás a profilhoz beállított fordítókkal:

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 \
    alignment_demo.c -o alignment-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -O2 \
    alignment_demo.c -o alignment-clang

Az első tároló tulajdonosa az owner által képviselt programállapot, a view nem tulajdonló hozzáférés. A free(owner) lezárja a tároló élettartamát. A nullázás csak a két helyi változót módosítja; esetleges további pointermásolatokat nem tenne biztonságossá.

A logikai adatméret 1000 marad, még ha az első foglalás 1024 C bájtos is. A többlet lefoglalt tároló nem automatikusan inicializált alkalmazásadat.

8.3.4 Lapigazítás és objektumigazítás

POSIX/Linux. A fájl- és anonim leképezéseknél megjelenő lapigazítás az operációs rendszer lapkezeléséhez kapcsolódik. Linuxon a null kezdőcímjavaslattal létrehozott sikeres mmap leképezés kezdete lapigazított. A rendszer lapméretét futás közben kell lekérdezni; nem általános állandóként 4096-nak tekinteni.

Legyen PP a lapméret és AA egy objektum megkövetelt igazítása. Ha

PmodA=0, P\bmod A=0,

akkor a lapigazított alapcím teljesíti az AA igazítást is. Egy ettől kk C bájtra kezdődő objektumhoz azonban még szükséges:

kmodA=0. k\bmod A=0.

Az alábbi feltételes Linux x86-64 laborpélda P=4096P=4096 lapmérettel számol:

Objektum helye Követelmény Következik az igazítás?
Leképezés kezdete 64 Igen
Kezdettől 12 C bájtra 16 Nem
Kezdettől 64 C bájtra 64 Igen
Leképezés kezdete 8192 Nem feltétlenül

A lapigazítás tehát nem helyettesíti az objektumelhelyezés ellenőrzését. A lefoglalt tartományba beleférés és a típus szerinti hozzáférés feltételei külön megmaradnak.

A leképezés tulajdonosa a munmap végrehajtásáért felel. A leválasztott részre mutató minden kölcsönzött pointer érvénytelenné válik. Az mmap hibajelzése MAP_FAILED, nem null pointer; a leképezést pedig nem szabad free segítségével felszabadítani. A részletes leképezési szerződést a Fájlrendszer-műveletek, mmap és tartósság tárgyalja.

8.3.5 Gyakori hibák és javításuk

„A cast megigazítja a címet.” Nem: nem változtatja meg a tároló helyét. Már a hibásan igazított objektumpointer-konverzió is sértheti a C követelményeit; nem csak a későbbi dereferálás veszélyes.

„A malloc nálam mindig 64-re igazított.” Néhány megfigyelt cím nem API-garancia. Kiterjesztett követelményhez megfelelő allokációs szerződés kell.

„Igazított foglalás után közönséges realloc használható.” A realloc nem ígéri az eredeti kiterjesztett igazítás megőrzését. Biztonságos növeléshez új igazított foglalás, a megtartandó adatok másolása, majd a régi tároló felszabadítása szükséges. Az eredeti tulajdonosi állapotot csak sikeres új foglalás után szabad feladni.

„Kézzel beljebb tolom a pointert, majd azt felszabadítom.” A belső cím nem válik allokációs alapcímmé. A free számára az eredeti visszakapott pointer kell; ezért a kézi igazítás külön owner- és view-nyilvántartást igényel.

8.3.6 Feladatok

  1. Magyarázó feladat. Miért nem változtatja meg a _Alignas(64) unsigned char data[128] deklaráció az unsigned char[128] típus igazítását? Garantáltan 64-re igazított-e a második elem?

  2. Kódolási feladat. Egy API count darab, element_size C bájtos elemhez kér tárolót. Írd meg az elemszorzás és az ezt követő igazítási kerekítés ellenőrzését. A nulla elemszám legyen elutasított bemenet.

  3. Diagnosztikai feladat. Egy függvény posix_memalign segítségével foglal, hibánál perror-t hív, siker esetén eltárolja a kezdőcímet és egy belső pointert. Később a belső pointert adja át free-nek. Mi a két szerződéshiba?

  4. Alkalmazási feladat. Egy leképezés lapmérete 4096 C bájt. Garantáltan teljesül-e a 128-as objektumigazítás a kezdettől 384 C bájtra?

8.3.7 Megoldások

8.3.7.1 Deklaráció és típus

A _Alignas az adott objektum deklarációjára vonatkozik, nem hoz létre új tömbtípust. _Alignof(unsigned char[128]) értéke 1. A második elem egy C bájttal követi a 64-re igazított kezdetet, ezért nem 64-re igazított. Ellenőrzési feltétel: külön vizsgáljuk az objektum kezdetét, az elemtípus igazítását és az elemeltolást.

8.3.7.2 Ellenőrzött elemszorzás

A következő C17 részlet a teljes példában szereplő rounded_size függvényt és headereket használja:

static size_t allocation_size(size_t count,
                              size_t element_size,
                              size_t alignment)
{
    if (count == 0 || element_size == 0) {
        return 0;
    }
    if (count > SIZE_MAX / element_size) {
        return 0;
    }

    return rounded_size(count * element_size, alignment);
}

Pozitív element_size mellett az osztásos feltétel biztosítja, hogy a szorzás ábrázolható. Ezután a kerekítés saját ellenőrzése védi az összeadást. Például 125 darab 8 C bájtos elem, 64-es igazítással 1024-es eredményt ad. A függvény az allocator igazítástámogatását nem ellenőrzi.

8.3.7.3 Hibakód és tulajdonos

A posix_memalign visszatérési kódját kell feldolgozni; a perror más hibához tartozó errno értéket jeleníthet meg. A free pedig kizárólag az eredeti foglalási pointert kaphatja. A belső pointer kölcsönzött hozzáférés, amelyet a felszabadítás érvénytelenít. Ellenőrzési feltétel: minden sikeres foglaláshoz pontosan egy megfelelő felszabadítás tartozzon.

8.3.7.4 Leképezésen belüli igazítás

Igen: 4096mod128=04096\bmod128=0, és 384mod128=0384\bmod128=0. Ez az igazítást bizonyítja, nem a teljes hozzáférés helyességét. Az objektumnak a leképezett tartományban is el kell férnie, és a leképezésnek a használat teljes ideje alatt fenn kell maradnia.

8.3.8 Rövid összefoglaló

Az igazítási döntés két kérdésből áll: mit követel az objektum, és mit vállal az allocator? A _Alignof, _Alignas és max_align_t az első kérdést tisztázza. A malloc, aligned_alloc, posix_memalign és mmap különböző választ ad a másodikra.

A helyes megoldás ellenőrzi a méretaritmetikát és az API-specifikus feltételeket, majd megőrzi az eredeti tulajdonosi pointert. Sem a cast, sem a lapigazítás, sem néhány kedvező mérési megfigyelés nem helyettesíti ezt a szerződést.