Alignment is a rule about where a value may be placed in memory.
Every value has a size. Many values also have an alignment requirement. The size tells you how many bytes the value occupies. The alignment tells you what kind of address is acceptable for the value.
For example, a u8 can usually live at any byte address. A u32 often wants to live at an address divisible by 4. A u64 often wants to live at an address divisible by 8.
This is not mainly a Zig rule. It comes from the machine. CPUs are usually faster and sometimes stricter when values are placed at properly aligned addresses.
Size vs Alignment
Size and alignment are related, but they are not the same thing.
const std = @import("std");
pub fn main() void {
std.debug.print("u8: size={}, align={}\n", .{ @sizeOf(u8), @alignOf(u8) });
std.debug.print("u32: size={}, align={}\n", .{ @sizeOf(u32), @alignOf(u32) });
std.debug.print("u64: size={}, align={}\n", .{ @sizeOf(u64), @alignOf(u64) });
}Typical output:
u8: size=1, align=1
u32: size=4, align=4
u64: size=8, align=8@sizeOf(T) tells you how many bytes a value of type T occupies.
@alignOf(T) tells you the default alignment requirement for type T.
An alignment of 4 means the address should be divisible by 4.
An alignment of 8 means the address should be divisible by 8.
Why Alignment Exists
Memory is addressed byte by byte, but CPUs usually load and store larger chunks.
If a u32 is stored at an address divisible by 4, the CPU can often load it cleanly in one operation.
address: 1000 1001 1002 1003
bytes: [ one u32 ]Address 1000 is divisible by 4, so this is aligned for a 4-byte value.
But this would be unaligned:
address: 1001 1002 1003 1004
bytes: [ one u32 ]Address 1001 is not divisible by 4.
Some CPUs can handle this, but it may be slower. Some CPUs may trap. Zig makes alignment visible because systems code must care about these details.
Alignment in Pointers
Pointer types can carry alignment information.
A normal pointer to u32 assumes the pointer is properly aligned for u32.
var x: u32 = 123;
const p: *u32 = &x;Here, p points to a valid u32. Zig knows the address is aligned correctly.
This matters when dereferencing:
const value = p.*;Dereferencing a pointer means reading or writing the value at that address. That operation is only valid if the pointer points to properly aligned memory for that type.
Seeing an Address
You can inspect a pointer address with @intFromPtr.
const std = @import("std");
pub fn main() void {
var x: u32 = 123;
const p = &x;
std.debug.print("address = {x}\n", .{@intFromPtr(p)});
std.debug.print("align = {}\n", .{@alignOf(u32)});
}The printed address will vary each time and on each machine.
The important idea is that a *u32 should point to an address compatible with @alignOf(u32).
Struct Padding
Alignment affects structs.
Consider this struct:
const Example = struct {
a: u8,
b: u32,
};You may think the size is:
1 byte for a
4 bytes for b
total: 5 bytesBut the actual size is often larger.
const std = @import("std");
const Example = struct {
a: u8,
b: u32,
};
pub fn main() void {
std.debug.print("size = {}\n", .{@sizeOf(Example)});
std.debug.print("align = {}\n", .{@alignOf(Example)});
}Typical output:
size = 8
align = 4Why 8?
Because b wants to start at an address divisible by 4. After a, Zig may insert padding bytes before b.
Memory may look like this:
a: 1 byte
padding: 3 bytes
b: 4 bytes
total: 8 bytesPadding bytes exist only to satisfy alignment.
Field Order Can Change Size
Field order can affect struct size.
const A = struct {
a: u8,
b: u32,
c: u8,
};
const B = struct {
b: u32,
a: u8,
c: u8,
};These structs contain similar data, but their layout may have different padding.
const std = @import("std");
const A = struct {
a: u8,
b: u32,
c: u8,
};
const B = struct {
b: u32,
a: u8,
c: u8,
};
pub fn main() void {
std.debug.print("A size = {}, align = {}\n", .{ @sizeOf(A), @alignOf(A) });
std.debug.print("B size = {}, align = {}\n", .{ @sizeOf(B), @alignOf(B) });
}A common result is:
A size = 12, align = 4
B size = 8, align = 4The second layout wastes less space because the largest field comes first.
This matters in large arrays of structs. Saving 4 bytes per item is small for one value, but large for millions of values.
Alignment and Arrays
Arrays store items one after another.
const data: [4]u32 = .{ 1, 2, 3, 4 };Each u32 must be properly aligned. Since each item has size 4 and alignment 4, this is straightforward.
For structs, padding matters because every array element must also keep the next element aligned.
const items: [1000]Example = undefined;If Example has size 8, then the array uses:
1000 * 8 = 8000 bytesEven if the visible fields only seem to need 5 bytes each.
The size of a type includes the padding needed for arrays to work correctly.
Explicit Alignment
Zig lets you request a specific alignment.
var buffer: [1024]u8 align(16) = undefined;This asks Zig to place buffer at an address aligned to 16 bytes.
This is useful for SIMD, operating system APIs, hardware devices, memory allocators, and binary protocols that require particular alignment.
You can also see alignment in pointer types:
*align(16) u8This means a pointer to u8 whose address is aligned to 16.
Most beginner code does not need explicit alignment. But systems code often does.
Alignment and Byte Buffers
A common low-level mistake is treating arbitrary bytes as a larger type.
Suppose you have a byte buffer:
var bytes = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 };It may be tempting to read part of it as a u32.
But a u32 pointer requires proper alignment. A random position inside a []u8 buffer may not be aligned for u32.
This is risky:
// Dangerous idea in low-level code:
// treat bytes[1] as the start of a u32The address of bytes[1] is one byte after the start of the array. Even if bytes itself is aligned, bytes[1] may not be aligned for u32.
When reading binary data, prefer explicit byte parsing:
const std = @import("std");
pub fn main() void {
const bytes = [_]u8{ 0x78, 0x56, 0x34, 0x12 };
const value = std.mem.readInt(u32, bytes[0..4], .little);
std.debug.print("{x}\n", .{value});
}This reads bytes as a little-endian u32 without pretending the byte buffer is already a properly aligned u32.
Alignment and C Interop
Alignment matters when working with C.
C structs have layout and alignment rules. Zig can match C layout with extern struct.
const CPoint = extern struct {
x: i32,
y: i32,
};Use extern struct when the struct must match a C ABI.
Do not assume a normal Zig struct has the same layout as a C struct unless you explicitly use the right representation.
Alignment is part of ABI compatibility. If Zig and C disagree about field layout or alignment, data may be read incorrectly.
Packed Structs
Zig also has packed structs.
const Packed = packed struct {
a: u8,
b: u32,
};A packed struct removes normal padding and stores fields more tightly.
That can be useful for bit-level formats or hardware registers.
But packed layout can also create unaligned fields. Accessing such fields may require extra care or generate different code.
Do not use packed struct just to save memory. Use it when the exact packed representation is required.
For ordinary data, normal structs are usually better.
Common Mistake: Assuming Struct Size
This assumption is often wrong:
struct size = sum of field sizesThe real rule is:
struct size = field sizes + padding needed for alignmentAlways check with @sizeOf when layout matters.
std.debug.print("{}\n", .{@sizeOf(MyStruct)});If the struct crosses an FFI boundary, file format, network format, or hardware boundary, treat layout as a design issue, not a guess.
Common Mistake: Ignoring Alignment After Pointer Casts
Pointer casts can create alignment problems.
If you cast a pointer from bytes to a larger type, you must know the address is correctly aligned.
Bad mental model:
The bytes are there, so I can view them as any type.Better mental model:
The bytes must have the right size, the right meaning, and the right alignment.All three matter.
Practical Rules
Use @sizeOf(T) to inspect size.
Use @alignOf(T) to inspect alignment.
Expect structs to contain padding.
Put larger fields earlier when memory layout matters.
Use extern struct for C layout.
Use packed struct only when packed representation is required.
Do not cast arbitrary byte buffers into typed pointers unless you know the alignment is valid.
For binary formats, parse bytes explicitly.
The Main Idea
Alignment is about valid addresses for values.
A type does not only say what a value means. It also affects where that value may safely live in memory.
Zig exposes alignment because Zig is used for systems programming, where layout matters. Most of the time, the compiler handles alignment for you. But when you work with pointers, binary data, packed structs, C interop, allocators, or hardware, alignment becomes part of correctness.