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.
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:
workspace objektum kezdete legalább 64 C bájtos
igazítású, de az unsigned char[1000] típus igazítása ettől
nem változik meg. _Alignof(unsigned char[1000]) továbbra is
1._Alignof(struct simd_block) legalább
64.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.
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.
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.
malloc és
SIMDISO 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.
aligned_allocISO 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:
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.
posix_memalignPOSIX, 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.
Legyen
a hasznos méret,
az igazítás, mindkettő C bájtban. Az aligned_alloc számára
szükséges legkisebb megfelelő méret:
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:
Ha , akkor . Egyébként a hozzáadandó kitöltés:
Az összeadás pontosan akkor ábrázolható size_t típusban,
ha
Bizonyításvázlat. Ha , akkor , így . Az számhoz éppen a következő -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 | Igazítás | Lefoglalt méret | Többlet |
|---|---|---|---|
| 1000 | 64 | 1024 | 24 |
| 1024 | 64 | 1024 | 0 |
| 1025 | 64 | 1088 | 63 |
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-clangAz 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.
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 a lapméret és egy objektum megkövetelt igazítása. Ha
akkor a lapigazított alapcím teljesíti az igazítást is. Egy ettől C bájtra kezdődő objektumhoz azonban még szükséges:
Az alábbi feltételes Linux x86-64 laborpélda 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.
„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.
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?
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.
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?
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?
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.
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.
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.
Igen: , és . 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.
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.