Alignment und Padding
Prozessoren unterstützen Lade- und Speicheroperationen mit festen Operandgrößen (z. B. 8, 16, 32, 64 Bit). Intern erfolgen Speichertransfers jedoch meist in Cache-Line-Größe. Ist ein Wert natürlich ausgerichtet, kann er effizient geladen werden. Fehlausgerichtete Zugriffe können -- abhängig von Architektur und Mikroarchitektur -- zusätzliche Zyklen verursachen oder zu Ausnahmen führen, insbesondere wenn sie Wort- oder Cache-Line-Grenzen überschreiten.
Jeder Datentyp hat eine geforderte Ausrichtung. Das nennt man in C Alignment. Ein int hat typischerweise Alignment 4 und muss an einer durch 4 teilbaren Adresse liegen, ein double hat typischerweise ein Alignment 8. Die genauen Werte hängen von der Plattform ab. Auf 64-Bit-Systemen beträgt das Alignment für viele Typen 8 Bytes, auf 32-Bit-Systemen oft 4 Bytes.
Um diese Vorgaben einzuhalten, fügt der Compiler bei Bedarf Füllbytes (Padding) ein. Fehlende Ausrichtung kann folgende Folgen haben:
- zusätzliche CPU-Taktzyklen pro Zugriff
- Laufzeitfehler oder Abstürze auf bestimmten Plattformen
- undefiniertes Verhalten bei falschem Zeigerzugriff
Alignment-Regeln und Datentypen
Jeder Typ in C hat unter anderem zwei Eigenschaften:
- Größe
- Ausrichtung (Alignment)
Das Alignment bestimmt, dass die Startadresse durch diese Byteanzahl teilbar sein muss. Sowohl Größe als auch Alignment lassen sich über Sprachmittel in C (sizeof, alignof) ermitteln.
Der Compiler richtet jedes Objekt nach seinen Alignment-Anforderungen aus, das auf der Plattform optimal ist (oft gleich der Typgröße). Die konkreten Werte sind plattformabhängig. Das Application Binary Interface (ABI) der Zielarchitektur definiert das Alignment; der C-Standard gibt keinen festen Wert vor. Dadurch können dieselben Typen auf verschiedenen Systemen unterschiedliche Ausrichtungen und Größen haben. (Quelle: ISO/IEC 9899:2024, § 6.2.8 Alignment of objects)
Typisch sind:
- Das Alignment eines Objekts ist mindestens 1.
- Für Strukturen gilt das größte Alignment eines ihrer Mitglieder.
- Für Arrays gilt das Alignment des Elementtyps.
- Die Gesamtgröße einer Struktur ist ein Vielfaches ihres Alignment-Werts, damit Arrays von Strukturen korrekt ausgerichtet bleiben.
Die folgende Tabelle zeigt typische Werte:
| Typ | x86-64 System V ABI (Größe / Alignment) | Microsoft x64 ABI (Größe / Alignment) | Arm AArch64 ABI (Größe / Alignment) |
|---|---|---|---|
char |
1 / 1 | 1 / 1 | 1 / 1 |
short |
2 / 2 | 2 / 2 | 2 / 2 |
int |
4 / 4 | 4 / 4 | 4 / 4 |
long |
8 / 8 | 4 / 4 | 8 / 8 |
float |
4 / 4 | 4 / 4 | 4 / 4 |
double |
8 / 8 | 8 / 8 | 8 / 8 |
long double |
16 / 16 | 8 / 8 | 16 / 16 |
Tabelle: Beispiele typischer Größen und Alignments
Aus der Tabelle ergibt sich, dass das Microsoft x64 ABI beim Typ long eine kleinere Größe und ein kleineres Alignment verwendet. Zudem ist long double im Microsoft x64 ABI mit double identisch, also 8 Bytes groß und 8 Bytes ausgerichtet. (Der Typ existiert nur aus Kompatibilitätsgründen und bietet keine höhere Genauigkeit.)
Das x86-64 System V ABI ist der Standard fast aller Unix-ähnlichen Systeme (Linux, BSD, macOS) auf AMD64-/Intel-64-Hardware. Windows verwendet dagegen das Microsoft x64 ABI, das unter anderem long double auf 8 Bytes festlegt.
Solche Unterschiede können dazu führen, dass identische Strukturen auf verschiedenen Plattformen unterschiedliche Größen haben.
Füllbytes und Padding
Damit Datentypen an den richtigen Speicheradressen liegen, fügt der Compiler bei Bedarf ungenutzte Bytes ein. Es entstehen Lücken zwischen Objekten, um das Alignment zu wahren. Wie und wo der Compiler diese Füllbytes einsetzt, oder auch nicht, hängt vom jeweiligen Kontext ab.
Lokale Variablen
Lokale Variablen auf dem Stack unterliegen keinen festen Anordnungsregeln. Der Compiler darf sie beliebig umsortieren:
#include <stdint.h>
void foo() {
uint8_t x = 42; // 1 Byte
uint16_t y = 314; // 2 Bytes
uint32_t z = 0xDEADBEEF; // 4 Bytes
}
Der Compiler kann diese Variablen:
- In Deklarationsreihenfolge ablegen und Lücken für das Alignment einfügen
- Umsortieren, um den Speicherverbrauch zu minimieren
- Nach anderen Kriterien (z. B. Registernutzung) anordnen
Die tatsächliche Speicheranordnung ist vollständig implementierungsdefiniert und kann sich zwischen Compiler-Versionen oder Optimierungsstufen ändern.
Globale Variablen mit möglichen Füllbytes
Globale Variablen sind separate Objekte. Der C-Standard garantiert, dass ihre Speicherreihenfolge der Deklarationsreihenfolge entspricht. Der Compiler darf sie nicht umsortieren und fügt bei Bedarf Füllbytes ein:
#include <stdint.h>
uint8_t global_a; // 1 Byte, Alignment 1
// 3 Füllbytes
uint32_t global_b; // 4 Bytes, Alignment 4
Zwischen global_a und global_b können ungenutzte Bytes liegen, damit global_b an einer durch 4 teilbaren Adresse beginnt.
Alignment und Padding bei Strukturen
Bei Strukturen muss der Compiler oft Füllbytes einfügen, um jedes Mitglied korrekt auszurichten. Bei Strukturen haben diese Füllbytes einen besonderen Namen: Padding. Das folgende Beispiel zeigt Padding:
#include <stdio.h>
#include <stdint.h>
struct S {
uint8_t a; // 1 Byte
// 3 Bytes Padding
uint32_t b; // 4 Bytes
uint8_t c; // 1 Byte
// 3 Bytes Padding (trailing)
};
int main() {
printf("Größe: %zu Bytes\n", sizeof(struct S)); // 📣 Größe: 12 Bytes
}
a belegt 1 Byte. Danach fügt der Compiler 3 Padding-Bytes ein, damit b korrekt auf eine 4-Byte-Grenze ausgerichtet ist.
b belegt 4 Bytes. Anschließend folgt c mit 1 Byte, danach wieder 3 Padding-Bytes, um die Strukturgröße auf ein Vielfaches des größten Alignment-Anforderers (uint32_t) zu bringen.
Die Struktur ist dadurch 12 Byte groß und vollständig ausgerichtet.
|a |# |# |# |b0 |b1 |b2 |b3 |c |# |# |#
Tabelle: Anordnung der Bytes im Speicher, # symbolisiert ein Füllbyte
Wichtig:
sizeofgibt die Gesamtgröße einer Struktur inklusive Padding zurück. Das Ergebnis hängt vom Alignment ab und umfasst immer das Padding, auch das am Ende der Struktur. Deshalb ist die Größe im Beispielsizeof(struct S)gleich 12 Byte und nicht nur die Summe der tatsächlichen Mitgliedergrößen (1 + 4 + 1 = 6). Die Gesamtgröße einer Struktur ist immer ein Vielfaches des größten Alignment-Werts eines ihrer Mitglieder.
Der C-Standard garantiert:
- Padding zwischen Mitgliedern, damit jedes korrekt ausgerichtet beginnt
- Trailing Padding am Ende für korrekte Array-Ausrichtung
- Erhalt der Mitgliederreihenfolge
- Kein Padding vor dem ersten Mitglied
Über Padding-Bytes dürfen keine Annahmen getroffen werden. Der Standard legt fest, dass diese Padding-Bytes (Quelle: ISO/IEC 9899:2024, § 6.2.6.1) :
- Unspezifizierte Werte enthalten (undefined values)
- Bei der Initialisierung von statischen oder thread-lokalen Objekten mit Nullen gefüllt werden
- Bei der Zuweisung möglicherweise nicht kopiert werden
Padding erhöht zwar den Speicherverbrauch, gewährleistet aber effiziente Zugriffe, vermeidet Fehler auf Architekturen ohne Misalignment-Unterstützung und richtet Arrays korrekt aus.
Alignment der gesamten Struktur
Das Alignment einer Struktur entspricht dem größten Alignment ihrer Mitglieder. So bleibt jedes Mitglied korrekt ausgerichtet, auch in Arrays oder verschachtelten Strukturen.
Im Beispiel oben erfordert uint32_t ein 4-Byte-Alignment. Deshalb wird auch struct S aus dem Beispiel insgesamt auf 4-Byte-Grenzen ausgerichtet und ihre Gesamtgröße auf ein Vielfaches von 4 Bytes aufgerundet.
Trailing Padding
Der Compiler fügt falls nötig am Ende einer Struktur trailing padding bytes ein, damit Array-Elemente an korrekten Speichergrenzen beginnen.
#include <stdint.h>
struct S {
uint8_t a; // 1 Byte
uint32_t b; // 4 Bytes
};
struct S array[2];
Im Array array muss das Mitglied a des zweiten Elements (array[1].a) wieder auf einer Adresse liegen, die für uint32_t-Zugriffe korrekt ausgerichtet ist.
Dazu fügt der Compiler hinter dem letzten Mitglied der Struktur bei Bedarf Padding ein.
So bleibt auch bei mehreren Strukturelementen im Array das Alignment jedes Mitglieds gewährleistet.
Alignment und Padding von Unions
Unions teilen sich denselben Speicherbereich für alle ihre Mitglieder. Alle Variablen beginnen an derselben Adresse, und immer nur ein Element ist zu einem Zeitpunkt gültig.
Damit alle Mitglieder korrekt ausgerichtet sind, richtet der Compiler die gesamte Union nach dem strengsten Alignment eines ihrer Elemente aus. Das Alignment einer Union entspricht also dem größten Alignment-Wert ihrer Mitglieder.
#include <stdint.h>
union U {
uint8_t a; // a: 1 Byte
uint32_t b; // b: 4 Bytes
uint64_t c; // c: 8 Bytes
};
uint64_t verlangt 8-Byte-Alignment, uint32_t nur 4 und uint8_t nur 1. Der Compiler richtet die Union daher auf 8 Bytes aus.
Die Größe der Union entspricht der Größe des größten Mitglieds, also 8 Bytes. Padding zwischen den Mitgliedern gibt es nicht, da sie denselben Speicherbereich nutzen. Am Ende kann jedoch trailing padding entstehen, damit Arrays solcher Unions korrekt ausgerichtet bleiben.
Verschachtelte Strukturen
Wird eine Struktur in einer anderen Struktur verwendet, richtet sich das Alignment der äußeren Struktur nach dem größten Alignment-Wert ihrer Mitglieder, einschließlich der inneren. Der Compiler sorgt damit dafür, dass auch eingebettete Strukturen korrekt ausgerichtet bleiben.
Ein Beispiel:
#include <stdint.h>
struct Inner {
uint8_t a; // 1 Byte
uint32_t b; // 4 Bytes
};
struct Outer {
uint16_t x; // 2 Bytes
struct Inner y;
};
Das Alignment von struct Outer entspricht dem von struct Inner, hier also 4 Bytes.
Zwischen x und y kann der Compiler Padding einfügen, damit y auf einer Adresse beginnt, die durch 4 teilbar ist.
Der Compiler sorgt dafür, dass auch eingebettete Strukturen korrekt ausgerichtet bleiben.
| x0 | x1 | # | # | a | # | # | # | b0 | b1 | b2 | b3 | # | # | # | #
Tabelle: Anordnung der Bytes im Speicher
Verschachtelte und anonyme Strukturen
Für verschachtelte und anonyme Strukturen gelten dieselben Regeln: Der Compiler richtet jedes eingebettete Objekt so aus, dass dessen Alignment-Anforderungen erfüllt sind.
#include <stdio.h>
#include <stdint.h>
struct S {
uint8_t a;
struct {
uint32_t b;
};
uint16_t c;
};
int main() {
printf("Größe von A: %zu Bytes\n", sizeof(struct S));
// Typische Ausgabe: 📣 Größe von A: 12 Bytes
}
Ein uint32_t erfordert typischerweise 4-Byte-Alignment. Nach a fügt der Compiler drei Padding-Bytes ein, damit b korrekt liegt. Nach b folgt c, und am Ende wird erneut aufgefüllt, damit die Gesamtgröße ein Vielfaches der strengsten Anforderung ist.
| a | # | # | # | b0 | b1 | b2 | b3 | c0 | c1 | # | #
Tabelle: Anordnung der Bytes im Speicher
Anonyme und verschachtelte Strukturen ändern die Regeln nicht; sie erleichtern nur den Zugriff. Das Alignment wird genauso bestimmt wie bei jeder anderen eingebetteten Struktur.
Alignment von Arrays, kein Padding
Ein Array übernimmt das Alignment seines Elementtyps. Da alle Elemente denselben Typ haben, liegt jedes direkt hinter dem vorherigen. Zwischen Array-Elementen gibt es kein Padding.
#include <stdint.h>
int main() {
uint16_t array[3] = {10, 20, 30};
}
array[1] beginnt genau 2 Bytes nach array[0], da uint16_t zwei Byte groß ist.
Dasselbe Prinzip gilt auch für Variable Length Arrays denn ihr Alignment richtet sich nach dem Elementtyp.
Mehrdimensionale Arrays folgen demselben Prinzip. Ein zweidimensionales Array ist ein Array von Arrays: Das äußere Array enthält Elemente, die selbst Arrays sind. Das Alignment des gesamten Arrays entspricht dem Alignment der inneren Arrays, also dem Alignment der Elemente.
Flexible Array Members
Flexible Array Members am Strukturende haben keine feste Größe. sizeof berücksichtigt sie nicht, und der Compiler fügt danach kein Padding ein.
struct Packet {
size_t len;
unsigned char data[];
};
Die Struktur selbst endet nach len. Das anschließende Array beginnt direkt im zugewiesenen Speicherbereich.
Damit entsteht keine Lücke zwischen Struktur und Nutzdaten, und zwar nicht, weil das Padding aktiv verhindert würde, sondern weil das Array außerhalb des eigentlichen Strukturtyps liegt.
Beim Allozieren wird der Speicher auf sizeof(struct Packet) + len gesetzt werden, sodass der Platz exakt ausreicht.
Kontrolle und Mechanismen für Alignment
Neben den automatischen Alignment-Regeln lässt sich die Ausrichtung mit alignof abfragen und mit alignas gezielt steuern.
Operator alignof
alignof (seit C23) ist ein Operator, der das erforderliche Speicher-Alignment eines Typs oder Ausdrucks angibt, also die Anzahl der Bytes, an denen dieser Typ im Speicher ausgerichtet werden muss.
Exemplarisch für int und double:
#include <stdio.h>
int main() {
printf("alignof(int) = %zu\n", alignof(int)); // alignof(int) = 4
printf("alignof(double) = %zu\n", alignof(double)); // alignof(double) = 8
}
Die Ausgabe zeigt die Byte-Ausrichtung jedes Typs. Damit lässt sich überprüfen, ob Strukturen oder benutzerdefinierte Typen korrekt ausgerichtet sind.
In älteren C-Standards (C11 und C17) lautet das Schlüsselwort _Alignof (seit C23 deprecated). Beide Varianten liefern denselben Wert; nur der Name wurde mit C23 vereinheitlicht. In C11 und C17 muss <stdalign.h> eingebunden sein, um alignof als Makro auf _Alignof zu erhalten. Für noch ältere C-Versionen (vor C11) gibt es keine standardisierte Möglichkeit, das Alignment direkt zu ermitteln. Doch es gibt proprietäre Erweiterungen, zum Beispiel +__alignof__(type)+ für GCC/Clang und +__alignof(type)+ für MSVC.
Position eines Mitglieds mit offsetof ermitteln
In einer Struktur liegen die einzelnen Mitglieder nicht direkt hintereinander, sondern an bestimmten Speicherpositionen.
Die Position eines Mitglieds relativ zum Strukturanfang heißt Offset. Wegen Padding variiert der Abstand. Das Makro offsetof (seit C89) aus <stddef.h> liefert den exakten Offset.
(Quelle: ISO/IEC 9899:2024, § 7.21 <stddef.h>)
Für Bitfelder führt dies zu undefined behavior.
Die allgemeine Form lautet:
offsetof(type, member)
Dabei ist:
type: der Strukturtyp, in dem sich das Mitglied befindetmember: der Name des Mitglieds, dessen Offset berechnet werden soll
Das Makro liefert den Offset in Byte als Wert vom Typ size_t.
Der Ausdruck ist eine Konstante und kann auch in statischen Prüfungen verwendet werden.
Beispiel: Offsets der einzelnen Mitglieder:
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
struct S {
uint8_t a; // 1 Byte
uint16_t b; // 2 Bytes, muss auf 2-Byte-Grenze ausgerichtet sein
uint32_t c; // 4 Bytes, muss auf 4-Byte-Grenze ausgerichtet sein
};
int main() {
printf("Offset von a: %zu\n", offsetof(struct S, a));
// 📣 Offset von a: 0
printf("Offset von b: %zu\n", offsetof(struct S, b));
// 📣 Offset von b: 2
printf("Offset von c: %zu\n", offsetof(struct S, c));
// 📣 Offset von c: 4
}
Nach a fügt der Compiler ein Padding von einem Byte ein, damit b korrekt auf einer 2-Byte-Grenze beginnt.
Auch c startet an einer 4-Byte-Grenze, was ein weiteres Alignment-Padding nach b erklären kann.
So wird sichtbar, wie die Mitglieder im Speicher angeordnet sind und wie der Compiler Alignment-Regeln einhält.
Analyse einer Struktur mit sizeof, alignof, offsetof
Mit sizeof und alignof lässt sich zusätzlich prüfen, wie sich diese Regeln auf die gesamte Struktur auswirken.
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdalign.h>
struct S {
uint8_t a; // 1 Byte
uint32_t b; // 4 Bytes, muss auf 4-Byte-Grenze ausgerichtet sein
uint16_t c; // 2 Bytes
};
int main() {
printf("sizeof(A) = %zu\n", sizeof(struct S));
// 📣 sizeof(A) = 12
printf("alignof(A) = %zu\n", alignof(struct S));
// 📣 alignof(A) = 4
printf("offsetof(a) = %zu\n", offsetof(struct S, a));
// 📣 offsetof(a) = 0
printf("offsetof(b) = %zu\n", offsetof(struct S, b));
// 📣 offsetof(b) = 4
printf("offsetof(c) = %zu\n", offsetof(struct S, c));
// 📣 offsetof(c) = 8
}
Nach a fügt der Compiler drei Füllbytes ein, damit b korrekt auf einer 4-Byte-Grenze beginnt.
Nach c folgen weitere zwei Bytes, damit die Gesamtgröße (sizeof(struct S)) ein Vielfaches des größten Alignment-Werts (4) bleibt. So wird das Padding innerhalb der Struktur und am Ende sichtbar und damit auch, wie der Compiler Speicherlayout und Ausrichtung kombiniert.
|Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11
|Inhalt | a | # | # | # | b0 | b1 | b2 | b | c0 | c1 | # | #
Tabelle: Anordnung der Bytes im Speicher
Layout-Optimierung mit und ohne Spracherweiterungen
Wie erläutert, fügt der Compiler automatisch Padding ein, um alle Mitglieder korrekt auszurichten. Bei einzelnen Variablen spielt das kaum eine Rolle, doch bei vielen Strukturen oder Arrays summiert sich der Speicherverbrauch. Darum lohnt es sich, das Layout zu prüfen und gegebenenfalls anzupassen.
Mitglieder in Strukturen umsortieren
Durch geschickte Reihenfolge der Mitglieder lässt sich Padding reduzieren, ohne das Verhalten zu ändern. Das ist nicht immer möglich, insbesondere, wenn die Struktur ein festes Binärlayout haben muss, etwa für Datei- oder Netzwerkformate. Aber bei internen Datenstrukturen kann man meist ohne Risiko optimieren.
Beispiel:
#include <stdio.h>
#include <stdint.h>
struct S {
uint8_t a; // 1 Byte
uint32_t b; // 4 Bytes
uint16_t c; // 2 Bytes
};
int main() {
printf("A: %zu Bytes\n", sizeof(struct S));
// Typische Ausgabe: 📣 A: 12 Bytes
}
Die Ursache: Nach a werden 3 Padding-Bytes eingefügt, damit b auf einer 4-Byte-Grenze liegt.
Nach c folgen 2 weitere, damit die Gesamtgröße ein Vielfaches von 4 bleibt.
| a | # | # | # | b0 | b1 | b2 | b3 | c | # | # | #
Tabelle: Anordnung der Bytes im Speicher
Sortiert man die Mitglieder absteigend nach Alignment, lässt sich Padding reduzieren:
#include <stdio.h>
#include <stdint.h>
struct T {
uint32_t b; // 4 Bytes
uint16_t c; // 2 Bytes
uint8_t a; // 1 Byte
};
int main() {
printf("B: %zu Bytes\n", sizeof(struct T));
// Typische Ausgabe: 📣 B: 8 Bytes
}
Die Struktur ist funktional identisch, benötigt aber ein Drittel weniger Speicher.
| b0 | b1 | b2 | b3 | c0 | c1 | a | #
Tabelle: Anordnung der Bytes im Speicher
Bei großen Arrays kann das mehrere Kilobyte oder Megabyte sparen.
Die Regel lautet: Große Mitglieder zuerst, kleine zuletzt.
Strukturen in Arrays
In Arrays wiederholt sich Padding pro Element. Eine 12-Byte-Struktur mit 10 Nutzbytes verschwendet bei 1 Million Einträgen etwa 2 MB. Eine optimierte Mitgliederreihenfolge verringert diesen Verlust.
Ausrichtungsspezifizierer alignas
Der Compiler nutzt natürliches Alignment für effiziente Zugriffe. In hardwarenaher oder hochoptimierter Software reicht das nicht immer und dann muss das Alignment gezielt erhöht werden.
Typische Gründe:
-
SIMD-Registerzugriffe: Vektorregister (SSE, AVX, NEON) erwarten Daten an einer Adresse, die durch ihre Registerbreite teilbar ist, meist 16, 32 oder 64 Byte. Fehlalignment verursacht Zusatzzyklen oder führt zu Ausnahmen. AVX-512 z. B. lädt und speichert effizient nur bei 64-Byte-Alignment.
-
GPU-Speicherzugriffe: Auf GPUs ist das Alignment von Daten für eine effiziente Speicherkoaleszierung entscheidend. (Quelle: Speicherkoaleszierung bedeutet, dass Zugriffe mehrerer Threads in einer Gruppe (wie einem Warp bei NVIDIA mit 32 Threads oder einer Wavefront bei AMD mit 64 Threads) zu einer einzigen Speichertransaktion kombiniert werden, um die Bandbreite optimal zu nutzen.) Typische Alignment-Grenzen liegen bei 128 oder 256 Bytes, da GPUs Speicher in solchen Blöcken verarbeiten. Dadurch können Threads Daten gemeinsam und effizient laden, ohne unnötige separate Transaktionen.
-
DMA-Transfers: Direkter Speicherzugriff durch Peripherie (Direct Memory Access) erlaubt es Hardware, Daten ohne laufenden CPU-Eingriff direkt zwischen Gerät und Hauptspeicher zu übertragen. Dafür müssen die verwendeten Adressen physisch korrekt ausgerichtet sein. Üblich sind Alignments von 32 oder 64 Byte, damit ganze Datenblöcke in einem Schritt übertragen werden können.
-
Atomare Operationen: Viele Architekturen verlangen, dass atomar zu verarbeitende Werte an Wort- oder Cache-Linien-Grenzen liegen. Nur dann garantiert die Hardware ungeteilte Lese- und Schreibzugriffe.
-
Cache-Linien-Grenzen: Liegen häufig genutzte Variablen in derselben Cache-Linie, kann ›false sharing‹ entstehen. Ein 64-Byte-Alignment trennt die Daten so, dass sie nicht unbeabsichtigt gemeinsam im Cache liegen.
Kurz gesagt: Manuelles Alignment ist nötig, wenn Performance, Hardwarekompatibilität oder Binärschnittstellen exakte Speichergrenzen erfordern.
Mit alignas (seit C11) lässt sich ein Mindest-Alignment vorgeben. Damit kontrolliert man, wie der Compiler Objekte im Speicher anordnet.
alignas kann an verschiedenen Stellen angewendet werden:
alignas(N) Typ name; // Variable
struct S { alignas(N) Typ mitglied; … }; // Einzelnes Mitglied
Sprachvergleich: In C++ darf man schreiben:
struct alignas(16) S { ... };. In C bezieht sichalignasauf Objekte, nicht auf Typen. Daher kann man einen Strukturtyp selbst nicht mitalignasdekorieren, sondern nur die Instanzen davon oder das erste Mitglied.
Der angegebene Wert für die Ausrichtung muss eine Zweierpotenz sein und darf nicht kleiner als die Alignment-Anforderung des Typs sein; andernfalls meldet der Compiler einen Fehler. Da das neue Alignment immer größer oder gleich dem Standard-Alignment ist, spricht man auch von Über-Alignment.
Hinweis:
alignasundalignofsind Geschwister:
alignofliefert das tatsächliche Alignment eines Typs oder Objekts,alignaslegt ein gewünschtes Alignment fest.Mit
alignoflässt sich also prüfen oder weiterverwenden, was durchalignasvorgegeben wurde.
Ein Beispiel illustriert den Effekt mit und ohne alignas:
#include <stdio.h>
#include <stdint.h>
#include <stdalign.h>
// alignas(32) // Auskommentiert für den Vergleich
uint8_t buffer[64];
struct Vec4 {
// alignas(16) // Auskommentiert für den Vergleich
float x, y, z, w;
};
int main() {
printf("alignof(uint8_t) = %zu\n", alignof(uint8_t));
printf("alignof(buffer) = %zu\n", alignof(buffer));
printf("alignof(struct Vec4) = %zu\n", alignof(struct Vec4));
printf("sizeof(struct Vec4) = %zu\n", sizeof(struct Vec4));
}
Ohne alignas (auf einem typischen System) ist die Ausgabe:
alignof(uint8_t) = 1
alignof(buffer) = 1
alignof(struct Vec4) = 4
sizeof(struct Vec4) = 16
Ohne alignas richtet sich der Compiler nach dem Typ, z. B. 1 Byte für uint8_t-Arrays oder 4 Bytes für float-basierte Strukturen.
Mit aktiviertem alignas(32) für buffer und alignas(16) für die Struktur ändert sich die Ausgabe:
alignof(uint8_t) = 1
alignof(buffer) = 32
alignof(struct Vec4) = 16
sizeof(struct Vec4) = 64 // Padding erhöht die Größe
Das explizite alignas erzwingt eine höhere Ausrichtung. Für die Struktur Vec4 führt das zu zusätzlichem Padding, da das Alignment der gesamten Struktur an das der Mitglieder angepasst wird.
Innerhalb von Strukturen wirkt sich alignas auf das Gesamtlayout aus und es beeinflusst Offsets, Padding und das Alignment der Struktur selbst. Das ist entscheidend für portable Codes, z. B. in ABIs oder Dateiformaten.
Hinweis: Ob
alignasinnerhalb der Struktur oder vor der Struktur steht, ändert, wo die Ausrichtung gilt:struct alignas(16) Vec4 { ... };richtet die gesamte Struktur auf 16 Byte aus.alignas(16) float x, y, z, w;(innerhalb der Struktur) betrifft nur das Mitglied selbst, sorgt aber indirekt dafür, dass auch die Struktur mindestens dieses Alignment erhält. Die Variante vorstructist klarer, wenn der ganze Typ ausgerichtet werden soll.
Mit C11 wurde alignas zunächst als Makro eingeführt, seit C23 ist es ein eigenes Schlüsselwort. Das zugrunde liegende Keyword _Alignas stammt ebenfalls aus C11 und gilt seit C23 als veraltet.
Vor C11 gab es keine standardisierte Möglichkeit zur Ausrichtung von Objekten. Stattdessen nutzten Compiler eigene Erweiterungen, zum Beispiel +__attribute__((aligned(n)))+ in GCC/Clang oder +__declspec(align(n))+ in MSVC.
Compiler-spezifische Packing-Attribute
Neben natürlichem Alignment und alignas lässt sich die Ausrichtung auch gezielt verringern, um Padding zu reduzieren. Das ist in einigen Situationen nötig:
- Wenn ein exaktes Byte-Layout vorgegeben ist, etwa bei Protokollstrukturen, Dateiformaten oder Memory-Mapped-I/O.
- Wenn Speicher oder Bandbreite begrenzt sind, zum Beispiel in Embedded-Systemen oder Netzwerkpuffern.
- Wenn Daten binär zwischen Systemen ausgetauscht werden und das Layout fest definiert ist.
Dazu kann das Alignment einer Struktur oder einzelner Mitglieder so angepasst werden, dass keine zusätzlichen Füllbytes mehr eingefügt werden.
Technisch geschieht das mit Compiler-spezifischen Attributen oder Pragmas:
-
#pragma pack(n)in MSVC, GCC oder Clang, wobeindie gewünschte Alignment-Grenze angibt (üblich 1 für maximale Komprimierung). -
+__attribute__((packed))+in GCC oder Clang, das das Alignment auf 1 Byte setzt.
Diese Mechanismen sind nicht portabel, da sie von Compiler zu Compiler variieren, und sie können die Laufzeitperformance beeinträchtigen. Grund dafür ist, dass nicht ausgerichtete Speicherzugriffe auf vielen Prozessorarchitekturen teurer sind oder sogar Ausnahmen auslösen, die vom Betriebssystem gehandhabt werden müssen.
Beispiel 1: Mitglieder unmittelbar hintereinander anordnen
In folgendem Beispiel steht das Mitglied b direkt hinter a, ohne dass der Compiler dazwischen Padding einfügen soll:
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
struct S {
uint8_t a; // Offset 0
uint32_t __attribute__((packed)) b; // direkt hinter a, kein Alignment
uint16_t c; // folgt unmittelbar nach b
};
int main() {
struct S x;
(void)x;
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
// 📣 sizeof(struct S) = 8
printf("Offset a = %zu\n", offsetof(struct S, a));
// 📣 Offset a = 0
printf("Offset b = %zu\n", offsetof(struct S, b));
// 📣 Offset b = 1
printf("Offset c = %zu\n", offsetof(struct S, c));
// 📣 Offset c = 6
}
Durch das Attribut +__attribute__((packed))+ wird das Alignment von b auf 1 Byte gesetzt, sodass es unmittelbar nach a beginnt. Nachfolgende Mitglieder wie c behalten ihr natürliches Alignment, was zu minimalem Padding führen kann.
Die Ausgabe verdeutlicht: Zwischen a (1 Byte) und b (4 Byte) gibt es kein Padding, denn b startet bei Offset 1 und liegt damit nicht auf einer natürlichen 4-Byte-Grenze. Die gesamte Struktur ist somit 8 Bytes groß.
| a | b0 | b1 | b2 | b3 | c0 | c1 | #
Tabelle: Anordnung der Bytes im Speicher
Ohne das Packed-Attribut würde b auf eine 4-Bytes-Grenze ausgerichtet (Offset 4), was 3 Bytes Padding nach a ergäbe und die Struktur auf 12 Bytes vergrößern würde.
Für eine vollständige Komprimierung der gesamten Struktur lässt sich das Attribut auf die Struktur selbst anwenden, wie hier:
struct __attribute__((packed)) S {
uint8_t a;
uint32_t b; // kein Alignment
uint16_t c; // kein Alignment
};
Dann wäre sizeof(struct S) 7.
| a | b0 | b1 | b2 | b3 | c0 | c1
Tabelle: Anordnung der Bytes im Speicher
Wie zuvor besprochen eignen sich gepackte Strukturen vor allem für Netzwerkprotokolle oder Dateiformate, bei denen Speicherplatz entscheidend ist, jedoch nicht für performanzkritische Berechnungen.
Beispiel 2: Strukturweite Packanweisung mit #pragma pack
Diesmal wird dieselbe Struktur einmal mit Standardausrichtung und einmal mit #pragma pack(1) definiert. Die Mitglieder sind identisch, nur das erzwungene Alignment unterscheidet sich.
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
// Standardausrichtung
struct S {
uint8_t a; // Offset 0
uint32_t b; // typ. Offset 4
uint16_t c; // typ. Offset 8
};
// Ab hier: Ausrichtung auf 1 Byte erzwingen
#pragma pack(push, 1)
struct T {
uint8_t a; // Offset 0
uint32_t b; // Offset 1
uint16_t c; // Offset 5
};
#pragma pack(pop)
int main() {
printf("sizeof(struct S) = %zu\n", sizeof(struct S));
// typisch 📣 sizeof(struct S) = 12
printf("sizeof(struct T) = %zu\n", sizeof(struct T));
// typisch 📣 sizeof(struct T) = 7
printf("Offsets in struct T: a=%zu, b=%zu, c=%zu\n",
offsetof(struct T, a),
offsetof(struct T, b),
offsetof(struct T, c));
// 📣 Offsets in struct T: a=0, b=1, c=5
}
#pragma pack(1) reduziert das Alignment der Struktur und ihrer Mitglieder auf 1 Byte. Der Compiler darf dadurch keine zusätzlichen Lücken zwischen den Mitgliedern einfügen. Die Reihenfolge der Bitfelder bleibt unverändert, aber die Allokationseinheiten werden dichter hintereinander im Speicher abgelegt.
Der push/pop Mechanismus stellt sicher, dass die Packanweisung nur für struct T gilt und nicht den Rest des Codes beeinflusst. Ohne pack hätte struct T typischerweise 12 Bytes belegt, wie bereits im ersten Beispiel zu sehen war.
Speicherallokation und Alignment
Bei statischen oder automatischen Variablen übernimmt der Compiler das Alignment selbst; er sorgt also dafür, dass jedes Objekt passend ausgerichtet ist. Anders ist es bei dynamisch reserviertem Speicher: Die Startadressen müssen korrekt ausgerichtet sein, sonst kann der Zugriff ineffizient oder ungültig sein.
Standard-Allokation (malloc())
malloc() liefert Speicher, der für alle eingebauten Typen korrekt ausgerichtet ist.
Das Standard-Alignment reicht also für jede Variable, die ohne besondere Vorgaben deklariert ist.
Damit lassen sich alle Typen bis zur Plattformgrenze max_align_t sicher speichern.
int *p = malloc(100 * sizeof(int));
Diese Zuweisung ist immer korrekt ausgerichtet, egal auf welcher Plattform. Das gilt auch für Strukturen, solange sie kein Über-Alignment erfordern. Erst wenn ein Typ mit alignas ein höheres Alignment verlangt, kann das zu Problemen führen.
Der Typ max_align_t aus <stddef.h> repräsentiert das maximale Alignment, das für jeden fundamentalen Typ garantiert ist. Typischerweise ist dies 16 Bytes auf modernen 64-Bit-Systemen, entsprechend long double oder SIMD-Typen.
#include <stdio.h>
#include <stddef.h>
int main() {
printf("alignof(max_align_t): %zu\n", alignof(max_align_t));
// 📣 alignof(max_align_t): 16
printf("sizeof(max_align_t): %zu\n", sizeof(max_align_t));
// 📣 sizeof(max_align_t): 32
}
Der Wert von max_align_t garantiert, dass malloc() Speicher zurückgibt, der für jeden fundamentalen Typ korrekt ausgerichtet ist.
Allokation mit festem Alignment
Für über-aligned Objekte stellt C (seit C11) die Funktion aligned_alloc() bereit. Die Signatur lautet:
void *aligned_alloc(size_t alignment, size_t size);
Die Größe (size) muss ein Vielfaches des Alignments sein.
Damit lässt sich Speicher mit einem expliziten Alignment reservieren.
Im folgenden Beispiel liefert die Funktion einen Zeiger, der mindestens auf einer 32-Byte-Grenze liegt:
void *p = aligned_alloc(32, 1024);
Beispiel:
#include <stdlib.h>
#include <stdalign.h>
#include <stdint.h>
#include <stdio.h>
typedef struct {
double data[4];
} Block;
int main() {
Block *p = aligned_alloc(32, sizeof(Block));
if (!p) return EXIT_FAILURE;
uintptr_t addr = (uintptr_t)p;
printf("addr=0x%zx, align=%d, size=%zu\n", addr, 32, sizeof(Block));
// 📣 addr=0x61a252a9e040, align=32, size=32
puts((addr % 32 == 0) ? "aligned" : "not aligned");
// 📣aligned
free(p);
}
Die Ausgabe zeigt, dass p auf einer 32-Byte-Grenze liegt.
Damit ist garantiert, dass SIMD-Instruktionen oder DMA-Operationen korrekt arbeiten.
Da aligned_alloc nicht auf allen Systemen vollständig implementiert oder verfügbar ist, existieren plattformspezifische Alternativen:
-
POSIX:
posix_memalign(&p, alignment, size):- Kein Vielfaches der Größe nötig.
- Gibt einen Fehlercode zurück (
0bei Erfolg). - Auf Unix-Systemen weitverbreitet und oft besser unterstützt.
-
Windows:
_aligned_malloc(size, alignment):- Gleiche Aufgabe, Freigabe mit
_aligned_free().
- Gleiche Aufgabe, Freigabe mit
Diese Varianten sind eine Alternative für Plattformen, auf denen aligned_alloc() fehlt. Sie erfüllen denselben Zweck: Speicheradressen mit definiertem Alignment bereitzustellen, etwa für SIMD- oder Cache-optimierte Daten.
Egal, mit welcher Funktion Speicher angefragt wurde, er muss grundsätzlich freigegeben werden. Es gibt aber einen kleinen Unterschied, wenn man die Windows API nutzt:
- Speicher aus
malloc(),calloc(),aligned_alloc()oderposix_memalignwird immer mitfree()freigegeben. - Speicher aus
_aligned_malloc(Windows) muss mit_aligned_free()freigegeben werden. Der Grund:_aligned_mallocnutzt eine eigene Implementierung, die nicht mitfree()kompatibel ist.
Alignment einer Adresse prüfen (memalignment())
Seit C23 gibt es die Funktion memalignment(), mit der sich das effektive Alignment einer beliebigen Speicheradresse bestimmen lässt. Die Funktion analysiert nur die Adresse selbst, unabhängig davon, wie der Speicher ursprünglich alloziert wurde.
size_t memalignment(const void *ptr);
Der Rückgabewert ist die größte Zweierpotenz, auf die ptr tatsächlich ausgerichtet ist. Das ist nützlich, wenn man prüfen möchte, ob externe Puffer, Hardware-Datenblöcke, DMA-Bereiche oder Speicher aus System-APIs das erwartete Alignment erfüllen.
Ein einfaches Beispiel:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main() {
void *p = aligned_alloc(64, 256);
if (!p) return EXIT_FAILURE;
size_t a = memalignment(p);
printf("Adresse: %p, Alignment: %zu\n", p, a);
free(p);
}
Damit lässt sich nachvollziehen, ob ein Puffer wirklich so ausgerichtet ist, wie die Allokationsfunktion es verspricht. Auch bei nicht selbst allokiertem Speicher, etwa bei Speicher aus Systemaufrufen oder externen Bibliotheken, liefert memalignment() eine einfache Möglichkeit zur Kontrolle.
Compilerunterstützung Werkzeuge
Compiler bieten mehrere Optionen, um Strukturen und deren Speicherlayout gezielt zu überprüfen. Dazu gehören Warnungen, die helfen, Probleme mit Padding und Alignment früh zu erkennen oder unnötige Attribute zu vermeiden.
Option -Wpadded: Füllbytes in Strukturen erkennen und vermeiden
Moderne Compiler erkennen Alignment- und Padding-Probleme, wenn entsprechende Warnungen aktiviert sind. GCC und Clang weisen mit der Option -Wpadded auf Padding in Strukturen hin.
(Quelle: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)
Unser bekanntes Beispiel:
#include <stdio.h>
#include <stdint.h>
struct S {
uint8_t a; // 1 Byte
uint32_t b; // 4 Bytes
uint16_t c; // 2 Bytes
};
int main() {
printf("sizeof(A): %zu\n", sizeof(struct S)); // 📣 sizeof(A): 12
}
GCC meldet:
<source>:6:14: warning: padding struct to align 'b' [-Wpadded]
6 | uint32_t b;
| ^
<source>:8:1: warning: padding struct size to alignment boundary ↩
with 2 bytes [-Wpadded]
8 | };
| ^
Clang ähnlich:
<source>:6:14: warning: padding struct 'struct S' with 3 bytes to ↩
align 'b' [-Wpadded]
6 | uint32_t b;
| ^
<source>:4:8: warning: padding size of 'struct S' with 2 bytes to ↩
alignment boundary [-Wpadded]
4 | struct S {
| ^
Wie wir vorher besprochen haben, können wir die Struktur umordnen, um damit Padding zu vermeiden. Dann verschwinden auch die Warnungen.
Während die Optionen -Wpadded GCC Clang eingefügtes Padding mledet nheißt bei
MSVC /d1reportSingleClassLayoutName zeigt Layoutdetails
Diese Hinweise helfen, Speicherlayout zu prüfen, bevor Probleme auftreten.
Option -Wpacked: Unnötige packed-Attribute in Strukturen vermeiden
Das Attribut +__attribute__((packed))+ verändert Strukturen so, dass der Compiler kein Padding zwischen den Mitgliedern einfügt. Das kann die natürliche Ausrichtung der Mitglieder verletzen.
Die GCC-Option -Wpacked warnt, wenn das packed-Attribut auf eine Struktur angewendet wird, es aber keinen Einfluss auf Layout oder Größe hat, die Struktur also auch ohne packed keine Füllbytes enthalten würde. In solchen Fällen bringt das Attribut keinen Nutzen, kann aber in verschachtelten Strukturen zu Fehlausrichtungen und damit zu unaligned Zugriffen führen.
Ein typischer Auslöser ist eine Struktur, deren Mitglieder von sich aus passend ausgerichtet sind:
#include <stdint.h>
struct __attribute__((packed)) A {
uint32_t a;
uint32_t b;
};
Beim Kompilieren mit -Wpacked meldet GCC eine Warnung wie diese:
(Quelle: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)
:
<source>:6:1: warning: packed attribute is unnecessary for 'A' [-Wpacked]
6 | };
| ^
GCC verortet solche Typattribute am Ende der Deklaration; deshalb steht die Markierung auf der }-Zeile. Clang gibt standardmäßig keine Warnung aus.
Das überflüssige Attribut sollte entfernt werden.
Option -Waddress-of-packed-member: Zugriff auf gepackte Mitglieder prüfen
Wenn eine Struktur mit +__attribute__((packed))+ definiert ist, kann das Nehmen der Adresse eines Mitglieds zu einer nicht ausgerichteten Adresse führen:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
struct __attribute__((packed)) A {
uint8_t a;
uint16_t b;
};
int main() {
struct S x = { 'A', 123 };
uint16_t *p = &x.b; // Warnung: Adresse eines gepackten Mitglieds
printf("%" PRIu16 "\n", *p);
}
GCC (auch ohne -Wall/-Wextra) meldet z. B.:
<source>:12:19: warning: taking address of packed member of 'struct S' ↩
may result in an unaligned pointer value [-Waddress-of-packed-member]
12 | uint16_t *p = &x.b;
| ^~~~
Clang ähnlich:
<source>:12:20: warning: taking address of packed member 'b' of ↩
class or structure 'A' may result in an unaligned ↩
pointer value [-Waddress-of-packed-member]
12 | uint16_t *p = &x.b;
| ^~~
Die Warnung ist in beiden Compilern standardmäßig aktiv und mit -Wno-address-of-packed-member kann man sie abschalten.
Wenn die Adresse eines gepackten Mitglieds benötigt wird, sollte der Wert stattdessen in eine korrekt ausgerichtete Variable kopiert werden, etwa mit memcpy(). Damit vermeidet man direkte Zeigerarithmetik auf gepackten Daten.
Ist das packed-Attribut gar nicht nötig, kann es entfernt werden. In vielen Fällen entsteht das Problem nur, weil Strukturen künstlich gepackt wurden, obwohl keine äußere Schnittstelle das erfordert. Alternativ lässt sich das Design so anpassen, dass die Adresse einzelner Strukturmitglieder gar nicht benötigt wird, etwa durch Zugriffsfunktionen, die den Wert intern kopieren oder verarbeiten.
Option -Wcast-align: Alignment-Probleme bei Pointer-Casts erkennen
Auch wenn ein Programm das Alignment korrekt einhält, kann ein falscher Pointer-Cast diese Ausrichtung verletzen und damit trotzdem fehlerhafte Zugriffe verursachen. Solche Fälle sind oft schwer zu erkennen, daher ist es hilfreich, wenn der Compiler davor warnt.
Die GCC/Clang-Option -Wcast-align kann vor Pointer-Casts warnen, die die erforderliche Ausrichtung des Zieltyps erhöhen.
(Quelle: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)
Das folgende Beispiel zeigt, dass ein Zeiger auf einen Typ mit geringerer Ausrichtungsanforderung (uint8_t*, Alignment 1) in einen Zeiger auf einen Typ mit höherer Ausrichtungsanforderung (uint16_t*, Alignment 2) umgewandelt wird.
Da hier aber ein Versatz von +1 verwendet wird, zeigt der neue Zeiger auf eine ungerade Adresse und verletzt damit die 2-Byte-Ausrichtung, die für uint16_t erforderlich ist:
#include <stdio.h>
#include <stdint.h>
int main() {
// Ein uint8_t-Array mit Alignment 1
uint8_t buffer[10] = {0};
// Cast erhöht Alignment-Anforderung (Warnung hier)
uint16_t *ip = (uint16_t *)(buffer + 1);
// Versuche, den Zeiger zu dereferenzieren.
// Kann auf misaligned Systemen abstürzen!
*ip = 42; // Potenziell gefährlicher Zugriff
printf("Wert: %u\n", *ip);
}
Beim Kompilieren mit -Wcast-align erzeugt Clang eine Warnung wie diese:
<source>:7:20: warning: cast from 'uint8_t *' (aka 'unsigned char *') to ↩
'uint16_t *' (aka 'unsigned short *') increases required alignment from ↩
1 to 2 [-Wcast-align]
7 | uint16_t *ip = (uint16_t*)(buffer + 1);
| ^~~~~~~~~~~~~~~~~~~~~~~
GCC gibt keine Warnung unter x86-64, wohl aber unter Arm. Der Grund ist, dass die x86-Architektur auch bei falsch ausgerichteten Zugriffen in der Regel korrekt arbeitet -- der Zugriff ist erlaubt, nur etwas langsamer. Arm dagegen verlangt für viele Datentypen eine korrekte Ausrichtung; ein falsch ausgerichteter Zugriff kann dort zu einem Laufzeitfehler oder Absturz führen. Deshalb stuft GCC den Cast auf x86 als unkritisch ein, meldet ihn aber auf Arm, wo das Verhalten tatsächlich fehlerhaft wäre. Daher meldet GCC unter Arm:
<source>:7:20: warning: cast increases required alignment of ↩
target type [-Wcast-align]
7 | uint16_t *ip = (uint16_t*)(buffer + 1);
| ^
Solche Warnungen sollte man beheben, indem man z. B. memcpy() verwendet, um Daten sicher zu kopieren, oder die Ausrichtung manuell überprüft.
Zusammenfassung: Wann man Alignment manuell steuern sollte
Manuelles Alignment sollte man nur in klar begrenzten Situationen einsetzen, um die Portabilität und Wartbarkeit des Codes nicht unnötig zu beeinträchtigen. Erforderlich ist es, wenn Hardware oder Software feste Speichergrenzen verlangen, etwa bei SIMD-Operationen, atomaren Zugriffen, DMA-Transfers, GPU-Speicher oder Geräten, die Daten nur an bestimmten Adressen verarbeiten können. In solchen Fällen stellt eine gezielte Ausrichtung sicher, dass Speicherzugriffe korrekt und ohne Zusatzzyklen erfolgen.
Auch bei extern definierten Datenstrukturen mit festem Layout kann manuelles Alignment notwendig sein. Das betrifft Dateiformate, Netzwerkprotokolle, Binärschnittstellen und Memory Mapped IO. Hier muss eine Struktur exakt bytegenau aufgebaut sein. Spezielle Compiler-Attribute können Padding verhindern, müsssen aber mit Vorsicht eingesetzt werden. Direkte Zugriffe auf gepackte Mitglieder sind riskant und sollten über temporäre Kopien in korrekt ausgerichtete Variablen erfolgen, um Fehlzugriffe und Leistungsverluste zu vermeiden.
In allen anderen Fällen genügt das natürliche Alignment, das der Compiler automatisch festlegt. Zusätzliche Vorgaben sind meist unnötig und erschweren die Pflege des Codes. Wenn Padding reduziert werden soll, ist eine Umordnung der Strukturmitglieder oft ausreichend: Typen mit größerem Alignment gehören an den Anfang, kleinere an das Ende. Das spart Platz, ohne die Ausrichtung zu verändern.
Jede manuelle Änderung der Ausrichtung muss im Quellcode nachvollziehbar dokumentiert werden. Die Dokumentation sollte den technischen Grund, gemessene Anforderungen aus Tests und den Zeitpunkt der Entscheidung enthalten. Das erleichtert spätere Anpassungen und Prüfungen.
Statische Analyse und Struktur-Layout-Visualisierung: Um Alignment- und Padding-Probleme zu erkennen, gibt es verschiedene Analysewerkzeuge:
- Clang-Tidy prüft Code statisch ohne Ausführung und erkennt ineffiziente Strukturlayouts.
- Pahole visualisiert das Speicherlayout aus kompilierten Binärdateien (benötigt DWARF-Debug-Informationen). Zeigt Offsets, Größen, Padding-Löcher und Cache-Line-Belegung detailliert an. Pahole kann auch Strukturen reorganisieren und Optimierungsvorschläge anzeigen.
- Clang -fdump-record-layouts gibt das Layout direkt beim Parsen aus. Zeigt Offsets und Gesamtgröße, Padding muss aus Differenzen abgeleitet werden. Vorteil: Keine kompilierte Binärdatei nötig. Nachteil: Zeigt alle Strukturen, keine gezielte Filterung.