Skip to content

CPU Architectures

The architecture part of a target tells Zig what kind of processor the program will run on.

The architecture part of a target tells Zig what kind of processor the program will run on.

In this target:

x86_64-linux-gnu

the architecture is:

x86_64

In this target:

aarch64-linux-musl

the architecture is:

aarch64

In this target:

wasm32-freestanding-none

the architecture is:

wasm32

The architecture affects instruction encoding, register names, calling convention, pointer size, alignment, atomic operations, and available CPU features.

A program can inspect its architecture at compile time:

const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    std.debug.print("arch = {s}\n", .{@tagName(builtin.cpu.arch)});
}

On an x86-64 target, this prints:

arch = x86_64

On an ARM64 target, it prints:

arch = aarch64

The architecture is not only a label. It changes what code can be generated.

For example, pointer size follows the target architecture. A 64-bit target has 64-bit pointers. A 32-bit target has 32-bit pointers.

const std = @import("std");

pub fn main() void {
    std.debug.print("pointer bits = {d}\n", .{@bitSizeOf(usize)});
}

On x86_64-linux-gnu, this normally prints:

pointer bits = 64

On wasm32-freestanding-none, this normally prints:

pointer bits = 32

usize is the unsigned integer type large enough to hold a pointer. Its size is selected by the target.

Sometimes code must choose a different implementation for a different architecture.

const builtin = @import("builtin");

pub fn cacheLineSize() usize {
    return switch (builtin.cpu.arch) {
        .x86_64 => 64,
        .aarch64 => 64,
        else => 64,
    };
}

This example is deliberately simple. Real machines may vary. Architecture is only one part of the answer. CPU model and features may also matter.

Zig exposes CPU information through builtin.cpu.

const std = @import("std");
const builtin = @import("builtin");

pub fn main() void {
    std.debug.print("arch = {s}\n", .{@tagName(builtin.cpu.arch)});
    std.debug.print("model = {s}\n", .{builtin.cpu.model.name});
}

A target may also include CPU features.

On x86-64, examples include SIMD extensions. On ARM, examples include crypto or floating-point extensions. These features affect which instructions are legal for the compiler to emit.

For ordinary programs, the architecture is selected by the -target option:

zig build-exe main.zig -target x86_64-linux-gnu
zig build-exe main.zig -target aarch64-linux-musl
zig build-exe main.zig -target wasm32-freestanding-none

For more specific control, Zig also accepts CPU settings. These are useful when optimizing for a known machine or when avoiding instructions unsupported by older hardware.

The important distinction is this:

architecture: the family of machine
cpu model:    a particular kind of machine in that family
features:     individual instruction-set capabilities

Most portable Zig code should depend on the architecture only at narrow boundaries. The rest of the program should use normal Zig types and standard-library functions.

A good place for architecture-specific code is a small module.

const builtin = @import("builtin");

pub fn wordBits() comptime_int {
    return switch (builtin.cpu.arch) {
        .x86, .arm, .wasm32 => 32,
        .x86_64, .aarch64, .riscv64, .wasm64 => 64,
        else => @bitSizeOf(usize),
    };
}

The caller does not need to know the target. It calls wordBits.

This is the same rule as for operating systems: isolate the difference.

The compiler already knows the target. Use that knowledge when it removes duplication or prevents mistakes. Do not scatter target tests through the program.

Exercise 17-9. Print builtin.cpu.arch and builtin.cpu.model.name.

Exercise 17-10. Print @bitSizeOf(usize) for a 64-bit target and a 32-bit target.

Exercise 17-11. Write a function that returns true for 64-bit architectures.

Exercise 17-12. Build the same source file for x86_64-linux-gnu, aarch64-linux-musl, and wasm32-freestanding-none.