Memory Management

Memory management in Zig is handled manually, similar to C and C++. By default, variables are allocated on the stack. However, should we need some dynamic memory, we can use allocators to allocate memory on the heap for use.

Allocation is handled by the std.mem.Allocatorstruct, which defines methods to allocate and free memory based on some underlying allocation strategy. The Zig standard library provides several different allocators with different strategies.

Allocators

Let's first go through a simple example using the general-purpose allocator.

General-Purpose Allocator

The general purpose allocator in Zig can be used for most purposes. Here we use the allocfunction of the allocator to allocate memory for 16 u8s (16 bytes of memory).

// We first create the general-purpose allocator.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

// Then we get the general `std.mem.Allocator` struct from it.
// This is what we'll call to (de)allocate memory.
const allocator = gpa.allocator();

// Let's allocate 16 bytes of memory.
const some_bytes: []u8 = try allocator.alloc(u8, 16);

// Maybe put a string into it.
std.mem.copyForwards(u8, some_bytes, "Hello, my world!");

// What's in the memory?
std.debug.print("{s}\n", .{some_bytes});

// Wait, don't we need to free the memory???

defer

Here's a brief digression to introduce the deferkeyword. This keyword can be used to execute an expression at the end of the current scope. If there are multiple defers in the same scope, they wil be executed in the reverse order from which they were introduced.

The following statements will produce the following console output.

Checking for leaks

The example we gave earlier wasn't complete. We didn't actually free the memory we allocated. Should the program have been more long-running (e.g., web server), we would have leaked memory. Luckily, the general-purpose allocator comes with a built-in way to check for leaks, which composes nicely with the defer keyword that we just learnt.

Now, the program should crash indicating where the memory leak took place. To fix this, we can use defer once more to free the memory at the end of the scope.

Fixed Buffer Allocator

This allocator takes in a slice of bytes and performs allocations on it. The example should make this clear.

Arena Allocator

The arena allocator wraps an existing allocator, using it the perform allocations. However, it doesn't perform any frees, instead free-ing all the memory it allocated at once upon deinit.

page_allocator

This is the most basic allocator. When you make an allocation, it will ask the OS for an entire page of memory, which makes this extremely space inefficient, and also not performant.

ArrayList

Let's explore memory management further by looking at a common data structure used in Zig programs: the humble ArrayList. This is Zig's implementation of a dynamically-sized array in the standard library.

Notice how we pass allocator into the constructor of the ArrayList. It will store the allocator and use it whenever it needs to allocate memory internally. This is quite different from C, where the allocator is assumed to be a global construct (e.g., mallocand free). In this way, and since std.mem.Allocator represents any allocator, we can separate the concerns of how to allocate memory from how to implement a dynamic list.

However, storing the allocator means that the ArrayListstruct takes up more space. There is another version called ArrayListUnmanagedthat doesn't require passing the allocator in the constructor. Instead, you pass the allocator each time you need to allocate memory.

And this highlight a common choice in Zig, whether to store the allocator or pass it in each memory-allocating operation. There isn't a correct answer, and really depends on the use-case. Hash maps in Zig also follow a similar principle, with the standard library providing both managed and unmanaged versions.

Last updated