Generate Zero‑Runtime Tables with Zig’s @compileTime
Use Zig’s @compileTime to generate static lookup tables at compile time, eliminating runtime initialization. The article walks through a Base64 example, explains limits, common mistakes, and practical verification steps.
14 Sept 2025, 05:09 UTC

Why Use @compileTime for Lookup Tables?
When a program needs a static mapping – for example, converting ASCII characters to Base64 values – the naïve approach is to hard‑code a 64‑element array or build it at startup. Zig’s @compileTime keyword lets you compute that array once, during compilation, and embed the result directly into the binary. The program then starts with the table already available, saving both memory allocation and initialization time.
How Zig Evaluates a Comptime Function
A function marked comptime (or invoked inside a @compileTime expression) is executed by the compiler instead of the runtime. The return value must be a compile‑time constant, so the compiler can replace the call with a literal. Inside the function you can use loops, recursion, and other pure operations, but you cannot perform I/O, allocate runtime memory, or call non‑comptime functions.
Worked Example: Base64 Lookup Table
The following program demonstrates a typical use case. It defines a comptime function that builds an array mapping ASCII codes to their Base64 6‑bit values. The table is then used by a runtime decode function.
const std = @import("std");
// Build a 64‑element array at compile time.
fn base64Table() comptime @compileTime {
const size = 64;
var table: [size]u8 = undefined;
for (0..size) |i| {
// Compute the ASCII code for each Base64 character.
const c = if (i < 26) 'A' + @intCast(u8, i)
else if (i < 52) 'a' + @intCast(u8, i - 26)
else if (i < 62) '0' + @intCast(u8, i - 52)
else if (i == 62) '+'
else if (i == 63) '/';
table[@intCast(u8, c)] = @intCast(u8, i);
}
return table;
}
// The table is a compile‑time constant.
const LOOKUP = base64Table();
// Runtime decoding uses the pre‑computed table.
pub fn decode(input: []const u8) ![]u8 {
var out = std.ArrayList(u8).init(std.heap.page_allocator);
for (input) |c| {
if (c == '=') break;
const val = LOOKUP[@intCast(u8, c)];
try out.append(val);
}
return out.toOwnedSlice();
}
pub fn main() !void {
const msg = "TWFu"; // "Man"
const decoded = try decode(msg);
std.debug.print("Decoded: {s}\n", .{decoded});
}
Running zig build-exe main.zig compiles the table into the binary. No runtime allocation is needed for the lookup; the compiler replaces every reference to LOOKUP with the literal array.
Verifying the Result
- Compile and run:
zig build-exe main.zig && ./mainshould printDecoded: Man. - Check compile time:
time zig build-exe main.zigand compare with a version that uses a hard‑coded array. The difference is usually negligible for small tables. - Inspect the binary: Use
objdump -s -j .rodata mainto confirm the table lives in read‑only data.
Limits of Comptime Functions
- Only compile‑time constants are allowed; no runtime variables or function parameters that depend on runtime values.
- Cannot perform I/O –
std.iocalls fail at compile time. - Cannot call non‑comptime functions or use the runtime allocator.
- Recursion depth is limited; deep recursion can trigger
comptime recursion depth exceedederrors. - Comptime code can increase compile time and binary size if overused.
Common Mistakes and How to Avoid Them
- Using @compileTime where a runtime value is expected. Example:
var x = @compileTime(foo());inside a function that runs at runtime will fail because the compiler expects a constant. - Assuming side effects are possible. Comptime functions cannot write to files, modify global state, or perform I/O.
- Ignoring recursion limits. For large tables, prefer iterative loops over recursive calls.
- Mixing runtime and compile‑time data. Keep the table entirely compile‑time; otherwise you’ll get a runtime allocation that defeats the purpose.
When to Use vs. When to Skip
Use @compileTime when:
- The data can be derived from constants and is small to medium in size.
- Runtime performance is critical and you want to avoid initialization overhead.
- You need compile‑time validation of constants (e.g., ensuring a string literal has a specific length).
Skip it when:
- The data is truly dynamic (depends on external files or user input).
- Generating the data is expensive and would noticeably increase build time.
- Binary size matters and the generated code would bloat the executable.
Practical Checklist Before Commit
- Run
zig build-infoto confirm the compiler version supports the syntax you used. - Measure compile time with and without the comptime code; if the difference exceeds your tolerance, consider lazy initialization.
- Run
zig teston the module to ensure the generated constants behave as expected. - Verify the binary size with
du -h mainand compare against a version with a manual table.
Conclusion
Zig’s @compileTime offers a clean, zero‑cost way to generate lookup tables and other static data. By moving work to the compiler, you eliminate runtime overhead and can enforce invariants at build time. Just keep an eye on compile time and binary size, and follow the limits to avoid common pitfalls.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.