Allocators

Allocators

The Zig language doesn't hide the memory management, the mantra of Zig is to have no hidden control flow. Like the C language, Zig has manual memory management however, Zig will offer the programmer different allocators that allow him to handle exactly how to use his memory. That's why Zig doesn't have a runtime and it can be used without the libc runtime.

Different allocators will be presented in the next sections.

This part is documented thanks to various resources:

General pattern

In Zig you will often see the pattern where you call a function and have to pass an allocator as a parameter, this is because of one of the main Zig mantras: "no hidden control flow". When you pass the allocator you know that the function is probably going to use it and make dynamic allocations.

In the Zig standard library, this pattern is used a lot.

For example, the ArrayList of the standard library uses this pattern :

    const stdout = std.io.getStdOut().writer();

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};

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

    try list.append(42);

    for (list.items) |value| {
        try stdout.print("Output:  {d}", .{value});
    }
Output:  42

Page allocator (page_allocator)

The page allocator is the basic allocator that will directly ask the OS for memory. This is not the most efficient allocator because it will request memory pages from the OS via systems calls.

  const allocator = std.heap.page_allocator;
  const stdout = std.io.getStdOut().writer();

  const MyStruct = struct {
      myFloat: f32,
      myInt: i32,
  };

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

  try stdout.print("Len of the memory: {}\n", .{memory.len});

  var myStruct = try allocator.create(MyStruct); 
  defer allocator.destroy(myStruct);

  myStruct.*.myFloat = 3.1415;
  myStruct.*.myInt = 42;

  try stdout.print("myStruct: {}\n", .{myStruct});
Len of the memory: 42
myStruct: Zig-src-RbksET.main.MyStruct{ .myFloat = 3.14149999e+00.myInt = 42 }

Fixed buffer allocator

The FixedBufferAllocator will allocate memory into a fixed buffer, the size of the buffer needs to be known at comptime. The benefit of this allocator is that it will not make heap allocation. It's very useful for embedded systems and kernel development. This allocator is very performant and it will give an error if the allocator runs out of memory, with an OutOfMemory error.

  const stdout = std.io.getStdOut().writer();

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

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

  try stdout.print("Len of the memory: {}\n", .{memory.len});

  // Example of OutOfMemory error
  _ = allocator.alloc(u8, 51) catch |err| {
    try stdout.print("There is an error: {}\n", .{err});
  };
Lenofthememory:50
Thereisanerror:error.OutOfMemory

Moreover, there is also a thread-safe fixed buffer allocator for thread-safety use case: std.heap.ThreadSafeFixedBufferAllocator.

Arena allocator

The arena allocator takes a child allocator as input. This pattern is used to allocate multiple pieces of memory and free them at once. There is no need in the arena allocator to free memory manually, it's the function deinit that is responsible for freeing all the allocated memory by this allocator.

The Zig documentation recommends this pattern when an application runs from start to end without a cyclic pattern. For example: command line application.

Here's an example of how to use the arena allocator.

  const stdout = std.io.getStdOut().writer();

  const MyStruct = struct {
      myFloat: f32,
      myInt: i32,
  };

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

  var myStruct = try allocator.create(MyStruct);

  myStruct.*.myFloat = 3.1415;
  myStruct.*.myInt = 42;

  try stdout.print("myStruct: {}\n", .{myStruct});
  // No need to manual free myStuct
myStruct: Zig-src-a2oNQA.main.MyStruct{ .myFloat = 3.14149999e+00.myInt = 42 }

Internal working of arena allocator

Zig how arena allocator works Internally, the arena allocator uses a linked list to keep track of the created buffers.

The example below shows a code snippet of the arena allocation (from the standard library arena_allocator.zig). The internals of the arena allocator are the child allocator and a state that contains a singly linked list of buffers.

  pub const ArenaAllocator = struct {
    child_allocator: Allocator,
    state: State,

    /// Inner state of ArenaAllocator. Can be stored rather than the entire ArenaAllocator
    /// as a memory-saving optimization.
    pub const State = struct {
        buffer_list: std.SinglyLinkedList(usize) = .{},
        end_index: usize = 0,

        pub fn promote(self: State, child_allocator: Allocator) ArenaAllocator {
            return .{
                .child_allocator = child_allocator,
                .state = self,
            };
        }
    };
    /// ...
  };

The deinit function of the arena allocator will free all the buffers by iterating over each node and calling the rawFree from the child allocator. (this code snippet comes from the standard library arena_allocator.zig)

      pub fn deinit(self: ArenaAllocator) void {
        // NOTE: When changing this, make sure `reset()` is adjusted accordingly!

        var it = self.state.buffer_list.first;
        while (it) |node| {
            // this has to occur before the free because the free frees node
            const next_it = node.next;
            const align_bits = std.math.log2_int(usize, @alignOf(BufNode));
            const alloc_buf = @as([*]u8, @ptrCast(node))[0..node.data];
            self.child_allocator.rawFree(alloc_buf, align_bits, @returnAddress());
            it = next_it;
        }
    }

General purpose allocator

A general purpose allocator is available in Zig, this is a safe allocator that can prevent double free memory, "use after free" and detect memory leaks. The general purpose allocator is safety first design, but it's still faster than the page allocator (Zig guide allocator)

Note that it aims to be even faster in the future.

The general purpose allocator is a function that takes as argument a comptime configuration struct and return a type. (this code snippet comes from the standard library general_purpose_allocator.zig)

  pub fn GeneralPurposeAllocator(comptime config: Config) type {
      return struct {
          /// Implementation ....
      };
  }

The configuration struct of the general purpose allocator shown below, has different options, like thread safety, memory limit, and debug utils. (this code snippet comes from the standard library general_purpose_allocator.zig)

pub const Config = struct {
    /// Number of stack frames to capture.
    stack_trace_frames: usize = default_stack_trace_frames,

    /// If true, the allocator will have two fields:
    ///  * `total_requested_bytes` which tracks the total allocated bytes of memory requested.
    ///  * `requested_memory_limit` which causes allocations to return `error.OutOfMemory`
    ///    when the `total_requested_bytes` exceeds this limit.
    /// If false, these fields will be `void`.
    enable_memory_limit: bool = false,

    /// Whether to enable safety checks.
    safety: bool = std.debug.runtime_safety,

    /// Whether the allocator may be used simultaneously from multiple threads.
    thread_safe: bool = !builtin.single_threaded,

    /// What type of mutex you'd like to use, for thread safety.
    /// when specified, the mutex type must have the same shape as `std.Thread.Mutex` and
    /// `DummyMutex`, and have no required fields. Specifying this field causes
    /// the `thread_safe` field to be ignored.
    ///
    /// when null (default):
    /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled.
    /// * the mutex type defaults to `DummyMutex` otherwise.
    MutexType: ?type = null,

    /// This is a temporary debugging trick you can use to turn segfaults into more helpful
    /// logged error messages with stack trace details. The downside is that every allocation
    /// will be leaked, unless used with retain_metadata!
    never_unmap: bool = false,

    /// This is a temporary debugging aid that retains metadata about allocations indefinitely.
    /// This allows a greater range of double frees to be reported. All metadata is freed when
    /// deinit is called. When used with never_unmap, deliberately leaked memory is also freed
    /// during deinit. Currently should be used with never_unmap to avoid segfaults.
    /// TODO https://github.com/ziglang/zig/issues/4298 will allow use without never_unmap
    retain_metadata: bool = false,

    /// Enables emitting info messages with the size and address of every allocation.
    verbose_log: bool = false,
};

The example below shows a basic usage of the Zig's GPA:

  const stdout = std.io.getStdOut().writer();

  const MyStruct = struct {
      myFloat: f32,
      myInt: i32,
  };

  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  const allocator = gpa.allocator();

  // Returns `Check.leak` if there were leaks; `Check.ok` otherwise.
  defer {
      const checkStatus = gpa.deinit();
      if (checkStatus == std.heap.Check.leak) {
          std.log.err("Leaks detected !!!", .{});
      }
  }

  var myStruct = try allocator.create(MyStruct);
  defer allocator.destroy(myStruct);

  myStruct.*.myFloat = 3.1415;
  myStruct.*.myInt = 42;

  try stdout.print("myStruct: {}\n", .{myStruct});
myStruct: Zig-src-MFk0Tx.main.MyStruct{ .myFloat = 3.14149999e+00.myInt = 42 }

Testing allocator

The testing allocator is available in tests and the test runner will report all the memory leaks that have occurred during testing.[1] [2]

The example below shows how to use the testing allocator.

  test "Test ArrayList" {
      var array = std.ArrayList(i32).init(std.testing.allocator);
      defer array.deinit();

      const expected: i32 = 42;
      try array.append(expected);

      try std.testing.expectEqual(expected, array.items[0]);
  }

If the code below is run, the test will fail and it will display a leaked test memory. Zig will help the programmer to detect memory leaks using code tests.

  test "Test ArrayList" {
      var array = std.ArrayList(i32).init(std.testing.allocator);
      //defer array.deinit(); -> the array will not be free

      const expected: i32 = 42;
      try array.append(expected);

      try std.testing.expectEqual(expected, array.items[0]);
  }

Under the hood, the testing allocator is an instance of the general purpose allocator. Below, an extract of testing allocator of the standard library testing.zig. If the testing allocator is used outside of the tests, a compilation error will be thrown.

  /// This should only be used in temporary test programs.
  pub const allocator = allocator_instance.allocator();
  pub var allocator_instance = b: {
      if (!builtin.is_test)
          @compileError("Cannot use testing allocator outside of test block");
      break :b std.heap.GeneralPurposeAllocator(.{}){};
  };

Failing allocator

The failing allocator can be used to ensure that the error.OutOfMemory is well handled.

The failling allocator need to have a child allocator to run. In fact, the failing allocator can set in his init function the number of allocation that will be performed without errors (see the numberOfAllocation variable). This pattern is pretty useful in restricted memory environments such as embedded development.

  test "test alloc falling" {
    const numberOfAllocation = 0;
    var failingAlloc = std.testing.FailingAllocator.init(std.testing.allocator, numberOfAllocation);
    var list = std.ArrayList(i32).init(failingAlloc.allocator());
    defer list.deinit();

    const expected = 45;

    try std.testing.expectError(std.mem.Allocator.Error.OutOfMemory, list.append(expected));
  }

C allocator

The C standard allocator can also be used, this allocator has high performance but it has less safety feature.

However, to use this allocator, the libC is required. Adding the libC in the project will add more dependencies.

Conclusion

Allocators are a great feature of Zig, they allow the programmer to have full control over the memory management while still having some nice tools to work with: like detecting memory leaks for exemple. The Zig allocators are not as restrictive as Rust's borrow checker, but they still come with great tools. On the other hand they are nice abstraction over C like heap allocations systems.

  1. Learning Zig - Heap Memory & Allocators, [Online]. Available: https://www.openmymind.net/learning_zig/heap_memory/ [Accessed: Mar. 3, 2024].
  2. ziglang/zig. Zig Programming Language, 2024. [Online]. Available: https://github.com/ziglang/zig [Accessed: Mar. 2, 2024].