Sid Ngeth's Blog A blog about anything (but mostly development)

Zig Memory Allocation

Memory Management in Zig: A Comprehensive Guide

Zig, a modern systems programming language, offers a unique approach to memory management. Unlike languages with garbage collection or those requiring manual memory management, Zig provides a set of tools and patterns that allow developers to choose the most appropriate memory management strategy for their specific needs. In this post, we’ll explore the various ways to allocate and manage memory in Zig, including both heap and stack allocation.

Heap Allocation Methods

1. General Purpose Allocator (GPA)

The General Purpose Allocator is Zig’s default allocator for most applications. It’s designed to be efficient for a wide range of allocation patterns.

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
    const leaked = gpa.deinit();
    if (leaked) std.debug.print("Memory leak detected!\n", .{});
}
const allocator = gpa.allocator();

const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);

Management: Manual deallocation required, but the GPA can detect leaks.

2. Arena Allocator

The Arena Allocator is perfect for scenarios where you need to allocate memory frequently and free it all at once.

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();

const memory1 = try allocator.alloc(u8, 100);
const memory2 = try allocator.alloc(u8, 200);
// No need to free memory1 or memory2 individually

Management: All memory is freed at once when arena.deinit() is called.

3. Fixed Buffer Allocator

When you have a fixed amount of memory to work with, the Fixed Buffer Allocator is an excellent choice.

var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();

const memory = try allocator.alloc(u8, 100);
// No need to free; memory is invalid when buffer goes out of scope

Management: No explicit deallocation needed; memory is reclaimed when the buffer goes out of scope.

4. Page Allocator

For large allocations, the Page Allocator directly interfaces with the operating system to allocate memory in page-sized chunks.

const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 4096);
defer allocator.free(memory);

Management: Manual deallocation required.

5. C Allocator

When interfacing with C libraries or when you need C-compatible allocation, the C Allocator is available.

const allocator = std.heap.c_allocator;
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);

Management: Manual deallocation required, follows C allocation patterns.

6. Allocator-Aware Types

Many types in Zig’s standard library are allocator-aware, managing their own memory when given an allocator.

var list = std.ArrayList(u32).init(allocator);
defer list.deinit();

try list.append(42);

Management: Memory is managed by the type itself, freed when deinit() is called.

7. Scratch Allocator

For temporary allocations within a single function call, the Scratch Allocator can be very efficient.

var scratch_buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&scratch_buffer);
var scratch = std.heap.ScratchAllocator.init(&fba);
const allocator = scratch.allocator();

const memory = try allocator.alloc(u8, 100);
// No need to free; memory is reclaimed when scratch goes out of scope

Management: Memory is automatically reclaimed when the scratch allocator goes out of scope.

8. Memory Pools

For scenarios where you need to frequently allocate and deallocate objects of the same size, a memory pool can be very efficient.

const MyStruct = struct { value: u32 };
var pool = std.heap.MemoryPool(MyStruct).init(allocator);
defer pool.deinit();

const obj = try pool.create();
defer pool.destroy(obj);

Management: Objects are returned to the pool when destroyed, the entire pool is freed on deinit().

9. Garbage Collection

While Zig doesn’t have built-in garbage collection, you can implement or use third-party GC libraries for specific use cases.

// Example using a hypothetical GC library
const GC = @import("gc");
var gc = GC.init(allocator);
defer gc.deinit();

const memory = try gc.alloc(u8, 100);
// No need to manually free; GC will handle it

Management: Automatic, handled by the garbage collector.

Stack Allocation

While heap allocation is powerful and flexible, Zig also provides efficient stack allocation for scenarios where it’s more appropriate. Stack allocation is faster and doesn’t require manual memory management, but it’s limited in size and scope.

fn stackExample() void {
    var buffer: [1024]u8 = undefined;
    var number: i32 = 42;
    // buffer and number are allocated on the stack and automatically freed when the function returns
}

Characteristics of Stack Allocation:

  • Fast allocation and deallocation
  • Memory is automatically managed
  • Limited in size (typically a few MB)
  • LIFO (Last In, First Out) structure

Use Cases for Stack Allocation:

  • Local variables
  • Function parameters
  • Small, fixed-size data structures
  • Short-lived data

Stack vs Heap: When to Use Which

Understanding when to use stack allocation versus heap allocation is crucial for efficient memory management in Zig.

Use Stack When:

  • You know the size of data at compile time
  • Data is small and short-lived
  • You need fast allocation/deallocation
  • You’re working with function-local data
  • You want to avoid memory fragmentation

Use Heap When:

  • Data size is unknown at compile time or can grow
  • You need data to persist beyond function calls
  • You’re working with large datasets
  • You need to share data between different parts of your program
  • You’re implementing complex data structures like trees or graphs

Example: Choosing Between Stack and Heap

Here’s an example that demonstrates how you might choose between stack and heap allocation based on runtime conditions:

fn example(allocator: std.mem.Allocator, input: []const u8) !void {
    if (input.len <= 1024) {
        var buffer: [1024]u8 = undefined;
        @memcpy(buffer[0..input.len], input);
        // Use buffer (stack allocation)
    } else {
        var dynamic_buffer = try allocator.alloc(u8, input.len);
        defer allocator.free(dynamic_buffer);
        @memcpy(dynamic_buffer, input);
        // Use dynamic_buffer (heap allocation)
    }
}

This example uses stack allocation for small inputs and falls back to heap allocation for larger inputs, demonstrating how Zig allows for flexible memory management strategies.

comments powered by Disqus