Zig can build the same source code in different optimization modes.
The mode controls speed, size, safety checks, and debug information.
The four common modes are:
| Mode | Main purpose |
|---|---|
Debug | Fast compilation, safety checks, useful debug info |
ReleaseSafe | Optimized code with safety checks |
ReleaseFast | Optimized code for speed |
ReleaseSmall | Optimized code for small size |
With zig build-exe, the default is Debug.
zig build-exe main.zigTo select a release mode, use -O:
zig build-exe main.zig -O Debug
zig build-exe main.zig -O ReleaseSafe
zig build-exe main.zig -O ReleaseFast
zig build-exe main.zig -O ReleaseSmallIn Debug, Zig keeps runtime safety checks.
For example, this program indexes past the end of an array:
const std = @import("std");
pub fn main() void {
const a = [_]u8{ 1, 2, 3 };
const i: usize = 9;
std.debug.print("{d}\n", .{a[i]});
}In a checked mode, this is caught as a safety problem.
Safety checks include bounds checks, integer overflow checks, invalid enum values, invalid error values, and other operations that Zig can guard at runtime.
ReleaseSafe keeps safety checks but optimizes the program.
zig build-exe main.zig -O ReleaseSafeThis is useful for production programs where correctness diagnostics are more important than the last measure of speed.
ReleaseFast optimizes for speed and disables many runtime safety checks.
zig build-exe main.zig -O ReleaseFastThis mode is appropriate only when the program has been tested enough that unchecked operations are acceptable.
ReleaseSmall optimizes for size.
zig build-exe main.zig -O ReleaseSmallThis is useful for embedded programs, WebAssembly, small command-line tools, and places where binary size matters.
The selected mode is visible at compile time:
const std = @import("std");
const builtin = @import("builtin");
pub fn main() void {
std.debug.print("mode = {s}\n", .{@tagName(builtin.mode)});
}This can be used to choose different behavior.
const builtin = @import("builtin");
pub fn expensiveCheck() bool {
if (builtin.mode == .Debug or builtin.mode == .ReleaseSafe) {
return true;
} else {
return false;
}
}Use this sparingly. Most code should not change behavior based on optimization mode.
A better use is diagnostics:
const std = @import("std");
const builtin = @import("builtin");
pub fn trace(comptime fmt: []const u8, args: anytype) void {
if (builtin.mode == .Debug) {
std.debug.print(fmt, args);
}
}In Debug, trace prints. In release builds, it does nothing.
Build mode is also part of the standard build.zig pattern:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "main",
.root_module = b.createModule(.{
.root_source_file = b.path("main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
}Then the mode is chosen from the command line:
zig build -Doptimize=Debug
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmallThe build file describes the program. The command line selects how it is built.
A good rule is:
Debug while writing.
ReleaseSafe while testing serious builds.
ReleaseFast when speed has been measured.
ReleaseSmall when size has been measured.Do not choose a release mode by habit. Choose it for the property you need.
Exercise 17-21. Print builtin.mode in each optimization mode.
Exercise 17-22. Write an array bounds error and compare Debug with ReleaseFast.
Exercise 17-23. Write a trace function that prints only in Debug.
Exercise 17-24. Build the same program with ReleaseFast and ReleaseSmall, then compare file sizes.