Skip to content

Alignment

Memory has addresses. Types also have alignment.

Memory has addresses. Types also have alignment.

The alignment of a type is the address boundary where values of that type are expected to live. An i32, for example, is usually stored at an address divisible by 4. A u64 is usually stored at an address divisible by 8.

Zig exposes alignment as part of the type system.

const std = @import("std");

pub fn main() void {
    std.debug.print("{d}\n", .{@alignOf(i32)});
}

This prints the alignment of i32 on the current target.

@alignOf(T) returns the alignment required by type T.

@alignOf(u8)
@alignOf(u16)
@alignOf(u32)
@alignOf(u64)

The exact values depend on the target. On common machines, u8 has alignment 1, u16 has alignment 2, u32 has alignment 4, and u64 has alignment 8.

A pointer carries alignment information.

var x: i32 = 10;
const p: *i32 = &x;

The type *i32 means a pointer to an i32 with the normal alignment for i32.

Sometimes a pointer has a weaker alignment.

const p: *align(1) i32 = @ptrCast(&x);

The type *align(1) i32 means a pointer to an i32, but the compiler may assume only byte alignment.

This distinction matters. A CPU may require aligned access. Even when unaligned access is allowed, it may be slower. More importantly, Zig uses alignment to decide which memory accesses are valid.

A pointer with stronger alignment can be used where weaker alignment is expected.

var x: i32 = 10;

const strong: *i32 = &x;
const weak: *align(1) i32 = strong;

This is safe because a normally aligned i32 pointer also satisfies alignment 1.

The reverse is not automatic.

const weak: *align(1) i32 = strong;
const strong_again: *i32 = weak; // error

The compiler cannot assume that weak is aligned enough for i32.

To assert stronger alignment, use @alignCast.

const strong_again: *i32 = @alignCast(weak);

This says that the address really satisfies the stronger alignment. If safety checks are enabled, Zig can check this at runtime in cases where the value is known only at runtime.

Alignment appears often when working with raw bytes.

var bytes: [16]u8 = undefined;
const p: *align(1) i32 = @ptrCast(&bytes[0]);

A byte array is aligned for bytes. It is not necessarily aligned for i32. Reading an i32 directly from arbitrary byte storage may require an alignment cast, or a safer operation that copies bytes into an aligned value.

The safe way to read structured data from bytes is often to copy.

const std = @import("std");

pub fn main() void {
    const bytes = [_]u8{ 1, 0, 0, 0 };
    const value = std.mem.readInt(u32, bytes[0..4], .little);

    std.debug.print("{d}\n", .{value});
}

This reads four bytes as a little-endian u32. It does not require treating the byte address as a *u32.

Packed structs also affect alignment.

const Header = packed struct {
    tag: u8,
    len: u32,
};

In a normal struct, fields may have padding so each field is suitably aligned. In a packed struct, fields are placed more tightly. A field inside a packed struct may not be naturally aligned.

For this reason, taking pointers to fields of packed structs needs care. The pointer may have a smaller alignment than the field type usually has.

Alignment is also important for allocators. An allocator must return memory aligned enough for the type that will be stored there.

const allocator = std.heap.page_allocator;

const p = try allocator.create(i32);
defer allocator.destroy(p);

p.* = 123;

create(i32) returns storage suitable for an i32. The allocator handles size and alignment.

For arrays, the allocation must be suitable for every element.

const items = try allocator.alloc(u64, 100);
defer allocator.free(items);

The returned slice is aligned for u64.

Most programs do not need to mention alignment explicitly. Values, arrays, structs, and allocator calls usually do the right thing.

Alignment becomes visible when code crosses into raw memory: byte buffers, binary formats, hardware registers, packed structs, and C interfaces.

The practical rule is simple: do not pretend that arbitrary bytes are already an aligned value. Either read the bytes explicitly, or prove the alignment and say so with the type.

Exercise 5-21. Print @alignOf(u8), @alignOf(u16), @alignOf(u32), and @alignOf(u64).

Exercise 5-22. Create a byte array and read a u32 from it using std.mem.readInt.

Exercise 5-23. Declare a pointer of type *align(1) i32 and try to assign it to *i32. Read the compiler error.

Exercise 5-24. Use @alignCast to convert a weakly aligned pointer back to a normally aligned pointer when the address is valid.