16 KiB
std.heap - Allocators
Zig has no default allocator. Functions that need heap memory accept an Allocator parameter.
Quick Reference
| Allocator | Use Case | Thread-Safe |
|---|---|---|
std.testing.allocator |
Unit tests (leak detection) | No |
std.heap.FixedBufferAllocator |
Stack-based, bounded size known | Optional |
std.heap.ArenaAllocator |
Batch free, CLI apps, request handlers | Allocator interface only, when child is thread-safe |
std.heap.page_allocator |
Backing for other allocators | Yes |
std.heap.c_allocator |
Linking libc, interop | Yes |
std.heap.DebugAllocator |
Debug builds, leak/corruption detection | Configurable |
std.heap.smp_allocator |
ReleaseFast production multithreaded | Yes |
std.heap.MemoryPool |
High-frequency same-type allocations | No |
std.heap.StackFallbackAllocator |
Stack buffer with heap fallback | Depends |
std.heap.wasm_allocator |
WebAssembly targets | Yes |
Allocator Naming Conventions
Using a generic allocator name hides memory ownership contracts. Name allocators by their memory contract to make code self-documenting:
| Name | Contract | Can Return Data? |
|---|---|---|
gpa |
Caller must free with defer gpa.free() |
Yes |
arena |
Bulk-deallocated at system boundary | Yes |
scratch |
Function-private temporary space | Never |
The Problem
// BAD - "allocator" says nothing about ownership
fn process(allocator: Allocator) ![]u8 {
const temp = try allocator.alloc(u8, 100); // Who frees this?
const result = try allocator.dupe(u8, temp); // Who owns this?
allocator.free(temp); // Is this correct?
return result; // Can caller free with same allocator?
}
The Solution
Name allocators by their contract:
// GOOD - names communicate ownership contracts
fn process(
gpa: Allocator, // General-purpose: caller must free returned data
scratch: Allocator, // Temporary: never return data allocated here
) ![]u8 {
// scratch is for intermediate computation only
const temp = try scratch.alloc(u8, 100);
defer scratch.free(temp);
// gpa for data that outlives this function
return try gpa.dupe(u8, computeResult(temp));
}
Full Example with All Three
fn handleRequest(
request: *Request,
arena: Allocator, // Response lifetime - bulk freed after response sent
gpa: Allocator, // Long-lived data - cache, shared state
scratch: Allocator, // This function only - intermediate computation
) !Response {
// Scratch: temporary parsing buffers (never escapes this function)
const parsed = try parseBody(request.body, scratch);
// GPA: update shared cache (outlives request)
try updateCache(gpa, parsed.cache_key, parsed.value);
// Arena: response data (freed when response completes)
const response_body = try formatResponse(arena, parsed);
return Response{ .body = response_body };
}
Common Patterns
CLI applications - arena for everything, freed at exit:
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
try run(arena.allocator()); // Name as "arena" - bulk freed at end
}
Request handlers - arena per request, gpa for shared state:
fn handleRequest(gpa: Allocator, request: Request) !Response {
var request_arena = std.heap.ArenaAllocator.init(gpa);
defer request_arena.deinit();
const arena = request_arena.allocator();
// arena: request-scoped data
// gpa: data that outlives the request (caches, connections)
}
Functions with temporary allocations - scratch parameter:
/// Computes result using scratch for intermediate work.
/// Caller owns returned slice (allocated from gpa).
fn compute(gpa: Allocator, scratch: Allocator, input: []const u8) ![]u8 {
const temp = try scratch.alloc(u8, input.len * 2);
defer scratch.free(temp);
// ... use temp for intermediate computation ...
return try gpa.dupe(u8, result);
}
Allocator Interface
const Allocator = std.mem.Allocator;
// Single items: create/destroy
const ptr: *T = try allocator.create(T);
defer allocator.destroy(ptr);
// Slices: alloc/free
const slice: []T = try allocator.alloc(T, count);
defer allocator.free(slice);
// Duplicate existing slice
const copy = try allocator.dupe(u8, source);
defer allocator.free(copy);
// Resize (returns bool - true if resized in place)
if (allocator.resize(slice, new_len)) {
// slice is now new_len (pointer unchanged)
}
// Reallocate (may move, returns new slice)
slice = try allocator.realloc(slice, new_len);
Choosing an Allocator
Decision flow:
- Library code? Accept
Allocatorparameter - let caller decide - Unit test? Use
std.testing.allocator(has leak detection) - Size known at comptime? Use
FixedBufferAllocatorwith stack buffer - Stack with heap fallback? Use
stackFallback(N, backing_allocator) - CLI app / one-shot? Use
ArenaAllocatorwrappingpage_allocator - Request loop (web/game)? Use
ArenaAllocator, reset per iteration - Many same-type objects? Use
MemoryPool(T)for fast create/destroy - Debug build? Use
DebugAllocatorfor leak/corruption detection - ReleaseFast production? Use
std.heap.smp_allocator - Linking libc? Use
c_allocator
Common Allocators
Testing Allocator
test "example" {
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, 100);
defer allocator.free(data); // Leak detected if missing!
}
FixedBufferAllocator
No heap allocations - allocates into a fixed buffer. Useful for kernels, embedded, or performance-critical code. Returns OutOfMemory when buffer exhausted:
var buffer: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const data = try allocator.alloc(u8, 100);
// Free/resize only works for most recent allocation
allocator.free(data);
// Reset to reuse buffer
fba.reset();
Thread-safe variant: allocation is lock-free. Resize, remap, and free are supported, but only the most recent allocation can be expanded or release arena space; shrinking an older allocation succeeds without reclaiming its bytes.
const ts_allocator = fba.threadSafeAllocator();
Ownership checks:
if (fba.ownsPtr(ptr)) { ... } // Check if pointer is within buffer
if (fba.ownsSlice(slice)) { ... } // Check if slice is within buffer
ArenaAllocator
Wraps a child allocator. Allocate many times, free all at once with .deinit(). Individual free() only works for most recent allocation:
// CLI app pattern - allocate freely, free all at end
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const data = try allocator.alloc(u8, 1000);
const more = try allocator.alloc(u8, 2000);
// No need to free individual allocations
}
// Request loop pattern - reset per iteration
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
while (running) {
_ = arena.reset(.retain_capacity); // Keep memory, reset state
const allocator = arena.allocator();
try handleRequest(allocator);
}
Reset modes:
.free_all- Release all memory to backing allocator.retain_capacity- Keep allocated pages for reuse (faster).{ .retain_with_limit = N }- Retain up to N bytes
Query retained capacity:
const retained_bytes = arena.queryCapacity(); // Excludes internal overhead
State optimization - store just the state to save memory:
const State = std.heap.ArenaAllocator.State;
var state: State = .{};
// Promote to full allocator when needed
var arena = state.promote(std.heap.page_allocator);
defer arena.deinit();
DebugAllocator
Detects leaks, double-free, use-after-free. Designed for safety over performance, but still faster than page_allocator. Safety checks and thread safety configurable:
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const check = gpa.deinit();
if (check == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const allocator = gpa.allocator();
Configuration options:
var gpa: std.heap.DebugAllocator(.{
.stack_trace_frames = 10, // Capture more frames
.enable_memory_limit = true, // Track total bytes
.safety = true, // Enable safety checks
.thread_safe = true, // Multi-thread support
.never_unmap = true, // Debug use-after-free
.retain_metadata = true, // Better double-free detection
}) = .init;
SmpAllocator
Maximum performance for multithreaded ReleaseFast builds. Few safety features:
const allocator = std.heap.smp_allocator;
const data = try allocator.alloc(u8, 1000);
allocator.free(data);
C Allocator
Alternative when smp_allocator is not available. Requires linking libc (-lc):
const allocator = std.heap.c_allocator;
Page Allocator
Requests entire pages from OS via syscall. A 1-byte allocation reserves multiple kibibytes - inefficient for small allocations. Use as backing allocator for ArenaAllocator or DebugAllocator:
const allocator = std.heap.page_allocator;
MemoryPool
Fast allocator for many objects of the same type. Outperforms general-purpose allocators when allocating/freeing objects in rapid succession:
const allocator = std.heap.page_allocator;
var pool: std.heap.MemoryPool(MyStruct) = .empty;
defer pool.deinit(allocator);
// Allocate objects (very fast)
const obj1 = try pool.create(allocator);
const obj2 = try pool.create(allocator);
// Free returns to pool for reuse (not to backing allocator)
pool.destroy(obj1);
// Reuses freed slot
const obj3 = try pool.create(allocator); // likely same address as obj1
// Reset all - batch destroy without individual frees
_ = pool.reset(allocator, .retain_capacity);
Options:
// Pre-allocate slots
var pool = try std.heap.MemoryPool(T).initCapacity(allocator, 100);
// Custom alignment
var pool: std.heap.memory_pool.Aligned(T, .@"64") = .empty;
// Non-growable (fixed capacity)
var pool = try std.heap.memory_pool.Extra(T, .{ .growable = false }).initCapacity(allocator, 50);
StackFallbackAllocator
Allocates from stack buffer first, falls back to another allocator when exhausted:
var fallback = std.heap.stackFallback(4096, std.heap.page_allocator);
const allocator = fallback.get();
// First 4KB comes from stack (no heap allocation)
const small = try allocator.alloc(u8, 100);
// Falls back to page_allocator if stack buffer exhausted
const large = try allocator.alloc(u8, 10000);
Wasm Allocator
Optimized for WebAssembly. Uses @wasmMemoryGrow:
const allocator = std.heap.wasm_allocator; // Only on wasm32/wasm64
Page Size Constants
std.heap.page_size_min // Comptime minimum page size for target
std.heap.page_size_max // Comptime maximum page size for target
std.heap.pageSize() // Runtime page size (may be comptime if min == max)
Passing Allocators
In libraries - accept allocator parameter:
pub fn MyContainer(comptime T: type) type {
return struct {
allocator: std.mem.Allocator,
data: []T,
pub fn init(allocator: std.mem.Allocator) @This() {
return .{ .allocator = allocator, .data = &.{} };
}
pub fn deinit(self: *@This()) void {
if (self.data.len > 0) {
self.allocator.free(self.data);
}
}
pub fn add(self: *@This(), item: T) !void {
// Use self.allocator for internal allocations
}
};
}
Functions returning allocated memory - document ownership:
/// Caller owns returned memory.
pub fn readFile(allocator: Allocator, path: []const u8) ![]u8 {
// ...
return try allocator.dupe(u8, content);
}
// Caller must free:
const content = try readFile(allocator, "file.txt");
defer allocator.free(content);
Common Patterns
Wrapping Allocators (Sub-Allocators)
// Arena on top of debug allocator
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
const allocator = arena.allocator();
Temporary Allocations in Loops
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
for (items) |item| {
// Reset arena each iteration for automatic cleanup
_ = arena.reset(.retain_capacity);
const temp = try arena.allocator().alloc(u8, item.size);
// temp is automatically "freed" on next reset
}
Sentinel-Terminated Allocations
// Allocate with null terminator
const str = try allocator.allocSentinel(u8, len, 0);
defer allocator.free(str);
// Duplicate with sentinel
const c_str = try allocator.dupeZ(u8, "hello"); // [:0]u8
defer allocator.free(c_str);
Error Handling
Always handle error.OutOfMemory:
// Option 1: Propagate
fn process(allocator: Allocator) !void {
const data = try allocator.alloc(u8, size);
defer allocator.free(data);
}
// Option 2: Handle gracefully
fn process(allocator: Allocator) void {
const data = allocator.alloc(u8, size) catch {
log.err("Out of memory", .{});
return;
};
defer allocator.free(data);
}
Zig 0.16 Notes
Primary release-note source: https://ziglang.org/download/0.16.0/release-notes.html
std.heap.ArenaAllocator.allocator()is thread-safe when its child allocator is thread-safe. Concurrent lifecycle, reset, state, and capacity-query operations are outside that guarantee; do not treat “lock-free” as a universal public contract.std.heap.ThreadSafeallocator was removed.- Allocators that perform blocking synchronization or file/entropy/time work should accept/store
std.Ioat initialization rather than constructing a local backend. - Keep allocator naming by memory contract (
gpa,arena,scratch) and expose storediothrough a small accessor only when callsites need it.
Initialization (0.16.x)
Use .init not .{}:
// WRONG - deprecated
var gpa: std.heap.DebugAllocator(.{}) = .{};
// CORRECT
var gpa: std.heap.DebugAllocator(.{}) = .init;
Debugging Memory Issues
Leak Detection
test "check for leaks" {
// std.testing.allocator automatically reports leaks
var list: std.ArrayList(u32) = .empty;
try list.append(std.testing.allocator, 42);
// Missing: list.deinit(std.testing.allocator);
// Test will FAIL with leak report
}
DebugAllocator in Main
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const check = gpa.deinit();
if (check == .leak) @panic("memory leak");
}
try run(gpa.allocator());
}
Implementing Custom Allocators
Allocators implement std.mem.Allocator.VTable:
const MyAllocator = struct {
// State fields here
pub fn allocator(self: *MyAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &vtable,
};
}
const vtable: std.mem.Allocator.VTable = .{
.alloc = alloc,
.resize = resize,
.remap = remap,
.free = free,
};
fn alloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ra: usize) ?[*]u8 {
const self: *MyAllocator = @ptrCast(@alignCast(ctx));
_ = ra; // return address for stack traces
// Return aligned pointer or null
}
fn resize(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) bool {
// Return true if resize succeeded in-place
}
fn remap(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 {
// Return new pointer (may move) or null if can't remap
}
fn free(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, ra: usize) void {
// Free memory
}
};
Validation wrapper - for testing allocators:
var my_alloc = MyAllocator.init();
var validated = std.mem.validationWrap(my_alloc.allocator());
const allocator = validated.allocator(); // Adds safety checks