🚀 Modern C (C11 → C23)
Alignment: why your struct is bigger than its parts
▶ Open the interactive lesson — free, no signupAsk C for a struct whose members add up to 6 bytes and sizeof reports 12 — half of it invisible empty space. In this lesson you find out where those hidden bytes go, and you pick up a 60-second member-reordering trick that can cut a program's memory use by a third when it stores millions of objects (game entities, chat messages, log records).
Add up the members of struct { char a; int b; char c; }: 1 + 4 + 1 = 6 bytes. Ask sizeof and you get 12. Where did 6 bytes go? The answer is alignment — one of those invisible rules that quietly shapes every byte of memory your program touches.
What alignment is
Hardware doesn't read memory one byte at a time; it moves fixed-size chunks (4, 8, 16 bytes) that start at addresses divisible by their size. A type's alignment requirement says which addresses it may live at: a 4-byte int wants an address divisible by 4, an 8-byte double one divisible by 8.
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
Why care? Three big reasons. Speed: a misaligned value can straddle two hardware chunks — two loads plus stitching instead of one. Correctness: some CPUs (older ARM, SPARC) flat-out fault on misaligned access, and in C, accessing a misaligned object is undefined behavior anyway. Special instructions: atomics and SIMD vector loads often require alignment — a 16-byte SSE load from a non-16-byte address crashes.
🧠 Checkpoint: A type with alignment 8 may live at which addresses?
- Any address
- Only even addresses
- Addresses divisible by 8
- Addresses ending in 8
Show answer
Addresses divisible by 8 — Alignment N means the address must be a multiple of N. 0x1000, 0x1008, 0x7ffe10 are fine for alignment 8; 0x1004 is not.
Querying: alignof
C11 added the keyword _Alignof (with a friendly alignof macro in <stdalign.h>; in C23 alignof is simply a keyword). It tells you any type's requirement:
#include <stdio.h>
#include <stdalign.h> /* alignof / alignas macros (C11–C17) */
#include <stddef.h> /* max_align_t */
struct mix { char c; double d; };
int main(void) {
printf("char : size %2zu align %2zu\n", sizeof(char), alignof(char));
printf("short : size %2zu align %2zu\n", sizeof(short), alignof(short));
printf("int : size %2zu align %2zu\n", sizeof(int), alignof(int));
printf("double : size %2zu align %2zu\n", sizeof(double), alignof(double));
printf("struct mix : size %2zu align %2zu\n", sizeof(struct mix), alignof(struct mix));
printf("max_align_t : size %2zu align %2zu\n", sizeof(max_align_t), alignof(max_align_t));
return 0;
}$ gcc -std=c17 alignof.c -o alignof && ./alignof char : size 1 align 1 short : size 2 align 2 int : size 4 align 4 double : size 8 align 8 struct mix : size 16 align 8 max_align_t : size 32 align 16 # a struct inherits the STRICTEST alignment among its members # (values are typical x86-64 Linux — alignment is implementation-defined)
max_align_t (from <stddef.h>) has the strictest alignment of any standard type — and malloc guarantees its results are aligned for it. That's why you can malloc anything without thinking about alignment.
Struct padding — and the reordering trick
Now the mystery solves itself. Inside a struct, every member must land on its own aligned offset, so the compiler inserts invisible padding bytes. And the struct's total size must be a multiple of its largest member's alignment (so arrays of it stay aligned). Here's our 12-byte struct:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
But padding depends on order, and order is yours to choose. Sort members largest-first and the holes vanish:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
Free memory optimization: ordering struct members from largest to smallest alignment never loses, and often shrinks the struct by 30–50%. For a struct allocated a million times, that's real megabytes. Confirm your layout with offsetof from <stddef.h> — and lock it in with a static_assert on sizeof!
🧠 Checkpoint: Why does the compiler pad struct { char a; int b; } to 8 bytes instead of 5?
- To leave room for future members
- So
bstarts at offset 4, and array elements stay aligned - Because sizeof must be a power of two
- It’s a gcc bug you can disable
Show answer
So b starts at offset 4, and array elements stay aligned — b needs a 4-divisible offset, so 3 pad bytes go after a. Tail padding then rounds the size to a multiple of the struct’s alignment so element [1] of an array is aligned too. (sizeof being a power of two is a coincidence here — a struct of 3 ints is 12.)
Forcing it: alignas and aligned_alloc
Sometimes the natural alignment isn't enough. Two threads hammering variables that share a 64-byte cache line will slow each other down ("false sharing"); SIMD code wants 16- or 32-byte buffers. C11's _Alignas / alignas over-aligns an object, and aligned_alloc does the same for the heap:
#include <stdio.h>
#include <stdlib.h>
#include <stdalign.h>
/* keep two hot counters on SEPARATE 64-byte cache lines */
struct counters {
alignas(64) long a;
alignas(64) long b;
};
int main(void) {
alignas(16) unsigned char simd_buf[64]; /* SSE-load ready */
struct counters c;
printf("&c.a = %p\n&c.b = %p\n", (void *)&c.a, (void *)&c.b);
printf("sizeof(struct counters) = %zu\n", sizeof(struct counters));
printf("simd_buf %% 16 == %lu\n", (unsigned long)simd_buf % 16);
/* heap version: 32-byte-aligned block (size = multiple of align!) */
double *v = aligned_alloc(32, 8 * sizeof(double));
printf("v %% 32 == %lu\n", (unsigned long)(void *)v % 32);
free(v);
return 0;
}$ gcc -std=c17 alignas.c -o alignas && ./alignas &c.a = 0x7ffc5a3c1e80 &c.b = 0x7ffc5a3c1ec0 sizeof(struct counters) = 128 simd_buf % 16 == 0 v % 32 == 0 # 0x...e80 and 0x...ec0 are 64 apart — each counter owns its cache line
Rules: alignas can only increase alignment (you can't ask for less than the type needs), and the value must be a power of two. For aligned_alloc(align, size), keep size a multiple of align — that's the portable contract — and free with plain free().
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
🧠 Checkpoint: What does malloc guarantee about alignment?
- Nothing — use aligned_alloc always
- Results are aligned for max_align_t, enough for any standard type
- Results are always 4096-byte aligned
- Alignment equal to the requested size
Show answer
Results are aligned for max_align_t, enough for any standard type — malloc returns memory suitably aligned for ANY object with fundamental alignment — i.e. aligned to alignof(max_align_t) (16 on typical x86-64). You only need aligned_alloc for OVER-alignment, like 32-byte AVX buffers or page-aligned I/O.
Alignment is about where values live; next we tackle a C11 feature about which code runs for each type — _Generic, C's answer to overloading.