Zig renamed GeneralPurposeAllocator to DebugAllocator in version 0.14.0, which broke every tutorial and Stack Overflow answer. The rename actually makes sense - they wanted to make it crystal clear this allocator is designed for catching bugs during development, not for production use.
DebugAllocator: Your new best friend (that you'll hate)
DebugAllocator is slow as hell but catches all the memory bugs that would otherwise ruin your weekend. It tracks every single allocation with stack traces, which is great when you're hunting a leak but makes your debug builds crawl.
Old way (pre-0.14.0 - this will break your build now):
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
Current way:
var debug = std.heap.DebugAllocator(.{}){};
defer _ = debug.deinit();
const allocator = debug.allocator();
The rename happened because too many people were using GeneralPurposeAllocator in production, which is fucking insane when you think about it. DebugAllocator makes it crystal clear: this is for finding bugs, not for shipping code.
Performance hit is real - my test suite went from 2.3 seconds to 11.8 seconds when I switched from page_allocator to DebugAllocator. But it caught 3 memory leaks I didn't even know existed, so whatever.
Other Allocators Available
For production code, you've got several options depending on what you're doing.
SmpAllocator works for multi-threaded stuff but honestly the docs are shit and I spent way too long figuring out when it's thread-safe vs when it isn't. There's some discussion on GitHub with actual numbers if you want to dive into the weeds.
ArenaAllocator is perfect when you have obvious cleanup points:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Frees everything at once
const allocator = arena.allocator();
It's basically a bump allocator - just keeps handing out chunks from a big buffer, then throws the whole thing away when you're done. Perfect for handling HTTP requests where you parse a bunch of JSON, do some work, send a response, then want to forget the whole thing ever happened.
What Each Allocator Actually Does
DebugAllocator (formerly GeneralPurposeAllocator):
var debug = std.heap.DebugAllocator(.{}){};
defer _ = debug.deinit(); // Prints leaks with stack traces
const allocator = debug.allocator();
What it catches:
- Memory leaks - shows exactly where you allocated memory and forgot to free it
- Double-free - detects when you call
free()
twice on the same pointer - Use-after-free - never reuses memory addresses, so accessing freed memory usually crashes immediately rather than silently corrupting data
FixedBufferAllocator for when you know exactly how much memory you need:
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// All allocations come from your buffer
const data = try allocator.alloc(u8, 512);
// When buffer is full, you get OutOfMemory
This is great for embedded development where you can't have unpredictable allocations.
Memory Debugging Workflow
Typical debugging process:
- Program crashes with segfault or corrupt memory
- Switch to DebugAllocator in debug builds
- Debug builds are much slower but give stack traces
- Find the bug location from the stack trace
- Fix the allocation/free mismatch
- Switch back to production allocator for release builds
A Basic Debugging Workflow
When you're tracking down memory bugs:
pub fn main() !void {
// Use DebugAllocator in debug builds
var debug = std.heap.DebugAllocator(.{}){};
defer {
const leak_status = debug.deinit();
if (leak_status == .leak) {
std.debug.print("Found memory leaks!
", .{});
}
}
const allocator = debug.allocator();
try runYourProgram(allocator);
}
When it finds leaks, it'll print stack traces showing exactly where you allocated memory without freeing it. Works great when the stack trace isn't corrupted - which is about 80% of the time.
Common Pitfalls
Missing leak detection: Writing defer _ = debug.deinit()
discards the return value and won't report leaks. Use:
defer {
const leak_status = debug.deinit();
if (leak_status == .leak) {
// Now you'll actually see the leaks
std.process.exit(1);
}
}
Arena allocator gotcha: ArenaAllocator doesn't free individual allocations. It keeps everything until you call arena.deinit()
. Perfect for request/response cycles, but don't use it in a long-running loop unless you want to eat all your memory.
FixedBufferAllocator stack overflow: Don't put huge buffers on the stack:
// This will overflow your stack
var buffer: [10 * 1024 * 1024]u8 = undefined; // 10MB on stack = crash
// Do this instead
var buffer = try std.heap.page_allocator.alloc(u8, 10 * 1024 * 1024);
defer std.heap.page_allocator.free(buffer);
var fba = std.heap.FixedBufferAllocator.init(buffer);
I learned about stack limits the hard way when I tried to allocate a 5MB buffer on the stack for processing images. Boom - instant segfault. The error message was useless: "Segmentation fault (core dumped)". No stack trace, no helpful hints, just death.
Took me 2 hours of debugging to figure out what was happening. Now I have a rule: anything over 64KB goes on the heap. The default stack size on Linux is usually 8MB, but you never know what weird environment your code might run in.