GeneralPurposeAllocator / FixedBufferAllocator
Among Zig's allocators, the two most commonly used in everyday development are GeneralPurposeAllocator and FixedBufferAllocator. The former is a general-purpose allocator with built-in leak detection for debug builds, while the latter carves memory from a fixed buffer on the stack without touching the heap at all. Choosing the right one for each use case lets you balance both safety and performance.
GeneralPurposeAllocator syntax
// -----------------------------------------------
// Basic pattern for GeneralPurposeAllocator (GPA)
// -----------------------------------------------
// The type parameter .{} uses default settings (leak detection is enabled in debug builds)
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// Call deinit() when leaving main to run the leak check
// It returns `.leak` so you can inspect the return value if needed
defer _ = gpa.deinit();
// Retrieve the std.mem.Allocator interface
const allocator = gpa.allocator();
// -----------------------------------------------
// Example with a common option
// -----------------------------------------------
// Disable thread safety to improve speed
var gpa_fast = std.heap.GeneralPurposeAllocator(.{ .thread_safe = false }){};
defer _ = gpa_fast.deinit();
// -----------------------------------------------
// Explicitly check for leaks via deinit()'s return value
// -----------------------------------------------
const leak_check = gpa.deinit();
if (leak_check == .leak) {
std.log.err("Memory leak detected", .{});
}
FixedBufferAllocator syntax
// ----------------------------------------------- // Basic pattern for FixedBufferAllocator (FBA) // ----------------------------------------------- // Prepare a buffer on the stack (adjust the size to suit your needs) var stack_buf: [1024]u8 = undefined; // Initialize FBA by passing the buffer var fba = std.heap.FixedBufferAllocator.init(&stack_buf); // Retrieve the std.mem.Allocator interface const allocator = fba.allocator(); // ----------------------------------------------- // reset() — rewind to the start of the buffer // ----------------------------------------------- // Calling reset() moves the internal pointer back to the start, // allowing the same buffer to be reused. // Do not use this if any pointers into the buffer are still live (dangling pointer risk). fba.reset(); // ----------------------------------------------- // Check how many bytes have been used // ----------------------------------------------- // The end_index field gives the current usage in bytes const used = fba.end_index;
Comparison and when to use each
| Allocator | Heap | Leak detection | How to free | Primary use case |
|---|---|---|---|---|
GeneralPurposeAllocator | Yes | Enabled in debug builds. | Call free() / destroy() individually. | General-purpose allocator for an entire application. |
FixedBufferAllocator | No | None (exceeding the buffer returns error.OutOfMemory). | Rewind the buffer with reset(). | Embedded environments or temporary stack-based buffers. |
ArenaAllocator | Yes (delegates to a parent) | Depends on the parent allocator. | Free everything at once with deinit(). | Allocating and releasing short-lived data in bulk. |
| Item | Description |
|---|---|
GeneralPurposeAllocator(.{}) | Creates a general-purpose allocator with default settings. Behavior can be changed via type parameters. |
gpa.deinit() | Releases the allocator and returns .leak if any memory was not freed. The idiom defer _ = gpa.deinit() is standard. |
gpa.allocator() | Returns the std.mem.Allocator interface. Pass this value to functions that need an allocator. |
FixedBufferAllocator.init(&buf) | Initializes FBA with an existing buffer. No heap allocation is performed. |
fba.reset() | Resets the internal pointer to the start of the buffer. Do not continue using old pointers after calling this. |
fba.end_index | Returns the number of bytes currently in use. Useful for tracking remaining buffer capacity. |
.thread_safe = false | An option to disable GPA's thread-safety features. Use this when running single-threaded and prioritizing speed. |
Sample code
kof_allocator.zig
// kof_allocator.zig — Example comparing GeneralPurposeAllocator and FixedBufferAllocator
// Uses THE KING OF FIGHTERS characters to demonstrate
// heap allocation with leak detection via GPA, and stack buffer allocation via FBA.
const std = @import("std");
// -----------------------------------------------
// Struct that holds fighter information
// -----------------------------------------------
const Fighter = struct {
name: []const u8, // Fighter name
team: []const u8, // Team name
power: u32, // Power rating
};
// -----------------------------------------------
// Allocates and returns a fighter's profile string using the given allocator.
// The caller is responsible for freeing the returned slice.
// -----------------------------------------------
fn buildProfile(allocator: std.mem.Allocator, f: Fighter) ![]u8 {
return std.fmt.allocPrint(
allocator,
"[{s}] {s} Power: {d}",
.{ f.team, f.name, f.power },
);
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
// -----------------------------------------------
// GeneralPurposeAllocator — general-purpose heap allocator
// In debug builds, it tracks allocation/free patterns and
// returns .leak from deinit() if any memory was not freed.
// -----------------------------------------------
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); // Run leak check when main exits
const gpa_alloc = gpa.allocator();
try stdout.print("=== KOF Fighter Profiles (GPA) ===\n\n", .{});
// Allocate Kyo Kusanagi's profile from GPA
const kyo_profile = try buildProfile(gpa_alloc, Fighter{
.name = "Kyo Kusanagi",
.team = "Japan Team",
.power = 9800,
});
defer gpa_alloc.free(kyo_profile); // Freed when the scope ends
try stdout.print("{s}\n", .{kyo_profile});
// Allocate Iori Yagami's profile from GPA
const iori_profile = try buildProfile(gpa_alloc, Fighter{
.name = "Iori Yagami",
.team = "Yagami Team",
.power = 9750,
});
defer gpa_alloc.free(iori_profile);
try stdout.print("{s}\n", .{iori_profile});
// Allocate Terry Bogard's profile from GPA
const terry_profile = try buildProfile(gpa_alloc, Fighter{
.name = "Terry Bogard",
.team = "Fatal Fury Team",
.power = 9500,
});
defer gpa_alloc.free(terry_profile);
try stdout.print("{s}\n", .{terry_profile});
// -----------------------------------------------
// GPA + create / destroy — allocate a single struct on the heap
// -----------------------------------------------
try stdout.print("\n--- Allocating a single fighter with create / destroy ---\n", .{});
// create allocates memory for one instance of Fighter and returns *Fighter
const boss = try gpa_alloc.create(Fighter);
defer gpa_alloc.destroy(boss); // Frees the pointer's memory when scope ends
boss.* = Fighter{
.name = "Rugal Bernstein",
.team = "Boss",
.power = 99999,
};
try stdout.print("BOSS: [{s}] {s} Power: {d}\n", .{ boss.team, boss.name, boss.power });
// -----------------------------------------------
// FixedBufferAllocator — fixed buffer on the stack
// Carves memory from a stack buffer without touching the heap.
// Well-suited for embedded systems and temporary stack-based processing.
// Exceeding the buffer returns error.OutOfMemory.
// -----------------------------------------------
try stdout.print("\n=== KOF Fighter Profiles (FBA) ===\n\n", .{});
// Prepare a 512-byte buffer on the stack
var stack_buf: [512]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&stack_buf);
const fba_alloc = fba.allocator();
// Allocate Nakoruru's profile from FBA (stack buffer)
const nakoruru_profile = try buildProfile(fba_alloc, Fighter{
.name = "Nakoruru",
.team = "Fatal Fury Team",
.power = 8800,
});
// FBA's free is a no-op internally, but we call it for interface consistency
defer fba_alloc.free(nakoruru_profile);
try stdout.print("{s}\n", .{nakoruru_profile});
// Allocate Mai Shiranui's profile from FBA
const mai_profile = try buildProfile(fba_alloc, Fighter{
.name = "Mai Shiranui",
.team = "Fatal Fury Team",
.power = 8900,
});
defer fba_alloc.free(mai_profile);
try stdout.print("{s}\n", .{mai_profile});
// Check how many bytes have been used
try stdout.print("\nStack buffer usage: {d} / 512 bytes\n", .{fba.end_index});
// -----------------------------------------------
// FixedBufferAllocator — reuse with reset()
// Calling reset() rewinds the pointer to the start,
// allowing the same buffer to be reused.
// Do not access old pointers after reset().
// -----------------------------------------------
try stdout.print("\n--- Reusing the buffer with FBA.reset() ---\n", .{});
// Finish accessing existing pointers before calling reset()
fba.reset(); // end_index returns to 0
try stdout.print("Usage after reset: {d} bytes\n", .{fba.end_index});
// Allocate new data after reset
const k_profile = try buildProfile(fba_alloc, Fighter{
.name = "K' (K-Dash)",
.team = "K' Team",
.power = 9600,
});
defer fba_alloc.free(k_profile);
try stdout.print("{s}\n", .{k_profile});
try stdout.print("Usage after reuse: {d} / 512 bytes\n", .{fba.end_index});
}
zig run kof_allocator.zig === KOF Fighter Profiles (GPA) === [Japan Team] Kyo Kusanagi Power: 9800 [Yagami Team] Iori Yagami Power: 9750 [Fatal Fury Team] Terry Bogard Power: 9500 --- Allocating a single fighter with create / destroy --- BOSS: [Boss] Rugal Bernstein Power: 99999 === KOF Fighter Profiles (FBA) === [Fatal Fury Team] Nakoruru Power: 8800 [Fatal Fury Team] Mai Shiranui Power: 8900 Stack buffer usage: 64 / 512 bytes --- Reusing the buffer with FBA.reset() --- Usage after reset: 0 bytes [K' Team] K' (K-Dash) Power: 9600 Usage after reuse: 32 / 512 bytes
Overview
Zig's GeneralPurposeAllocator is the most commonly used allocator for an entire application. In debug builds, it tracks allocation and deallocation patterns and returns .leak from deinit() if any memory was not freed. In release builds, the overhead is reduced and it runs faster. FixedBufferAllocator sits at the opposite end of the spectrum: it carves memory from a fixed array on the stack without touching the heap at all. Any allocation that exceeds the buffer is returned as error.OutOfMemory, making it suitable for embedded environments and performance-critical code where heap allocation must be avoided. Calling reset() rewinds the internal pointer to the start of the buffer so it can be reused, but if any old pointers are still live this creates a dangling pointer, so you must finish all accesses to old data before calling reset(). Both allocators implement the std.mem.Allocator interface, so by typing function parameters as std.mem.Allocator you can swap between them freely. See also Allocator basics for the fundamental allocator operations.
If you find any errors or copyright issues, please contact us.