View Source Allocations

Zig the language has no offically supported allocator, and the standard library datastructures are all allocator-agnostic.

Zigler ships with three primary allocators, though you can certainly build allocator strategies on top of those allocators.

Basic Allocator

The first allocator is allocator. This allocator wraps the nif allocator provided by the BEAM in the zig allocator interface. You should generally use this allocator over malloc because it often saves a syscall by using existing preallocated memory pools, because it allows the VM to track how much memory your NIF is using, and possibly gives better memory placement to avoid cache misses in your execution thread.

~Z"""
const beam = @import("beam");

pub fn allocate_raw(count: usize) !beam.term {
    const slice = try beam.allocator.alloc(u16, count);
    defer beam.allocator.free(slice);

    for (slice, 0..) |*entry, index| {
        entry.* = @intCast(index);
    }
    return beam.make(slice, .{});
}
"""

test "raw allocator" do
  assert [0, 1, 2] = allocate_raw(3)
end

allocator limitations

because the basic allocator directly wraps the beam allocator, according to the documentation:

The returned pointer is suitably aligned for any built-in type that fit (sic) in the allocated memory.

attempting to allocate memory aligned to a larger size (e.g. page-aligned allocation) will fail using this allocator.

Tracking memory.

information in hidden globals

Generally storing information in hidden globals is not a good idea. Here it is done to illustrate the memory usage. A better strategy would be to use resources

~Z"""
var global_zigler: []u8 = undefined;

pub fn zigler_alloc() !void {
    global_zigler = try beam.allocator.alloc(u8, 1_000_000);
}

pub fn zigler_free() void {
    beam.allocator.free(global_zigler);
}

const c_stdlib = @cImport(@cInclude("stdlib.h"));

var global_cstd: [*c]u8 = undefined;
pub fn c_malloc() void {
    global_cstd = @ptrCast(c_stdlib.malloc(1_000_000));
}

pub fn c_free() void {
    c_stdlib.free(global_cstd);
}
"""

test "zigler memory is tracked" do
  Process.sleep(100)
  start = :erlang.memory[:total]
  zigler_alloc()
  assert :erlang.memory[:total] - start >= 1_000_000
  zigler_free()
end

test "malloc memory is not tracked" do
  Process.sleep(100)
  start = :erlang.memory[:total]
  c_malloc()
  assert :erlang.memory[:total] - start <= 1_000_000
  c_free()
end

Wide Alignment Allocator

Zigler provides a wide_alignment_allocator which allows you to allocate memory ranges that have a higher alignment than the maximum alignment for builtin types.

memory penalty

Note that using this allocator comes with a memory penalty, so use as a general allocator is not recommended.

~Z"""
pub fn allocate_large_aligned(count: usize) !usize {
    const page = try beam.wide_alignment_allocator.allocWithOptions(u8, count, 4096, null);
    defer beam.wide_alignment_allocator.free(page);

    return @intFromPtr(page.ptr);
}
"""

test "aligned allocation" do
  assert 0 = rem(allocate_large_aligned(3), 4096)
end

General Purpose Allocator

Zigler provides a version of the zig standard library's GeneralPurposeAllocator which is built on top of the large allocator. Two advantages of using the general purpose allocator include optimized memory layouts for mixed allocation sizes and the ability to track memory leaks.

The state of the global general purpose allocator is accessible using beam.allocator_.general_purpose_allocator_instance

You may also create a custom general purpose allocator instance using beam.make_general_purpose_allocator_instance, whcih is what happens on a per-nif basis if the nif is checking for leaks.

~Z"""
pub fn leaks() !bool {
    const memory = try beam.general_purpose_allocator.alloc(u8, 8);
    defer beam.general_purpose_allocator.free(memory);

    // note that we haven't freed it yet, that happens on deferral,
    // which lands after the return call.

    return beam.allocator_.general_purpose_allocator_instance.detectLeaks();
}

pub fn noleak() !bool {
    const memory = try beam.general_purpose_allocator.alloc(u8, 8);
    beam.general_purpose_allocator.free(memory);
    return beam.allocator_.general_purpose_allocator_instance.detectLeaks();
}
"""

test "leak checks with general purpose allocator" do
  require Logger
  Logger.warning("====== the following leak message is expected: =========== START")
  Process.sleep(200)
  assert leaks()
  Logger.warning("=========================================================== END")

  refute noleak()
end

Custom allocators

Because zigler's allocators conform to zig's allocator interface, you can use any composed allocator in the standard library or any composable allocator from an imported zig package.

~Z"""
pub fn with_arena() !beam.term {
    const std = @import("std");

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

    const allocator = arena.allocator();

    const slice = try allocator.alloc(u16, 4);
    defer allocator.free(slice);

    for (slice, 0..) |*item, index| {
        item.* = @intCast(index);
    }

    return beam.make(slice, .{});
}
"""

test "arena allocator" do
  assert [0, 1, 2, 3] == with_arena()
end

Custom allocators in beam.get

If you choose to use a custom allocator, you may use it in the beam.get functions to instantiate data where it's the allocator's responsibility to free it at the end.

~Z"""
pub fn arena_sum(array: beam.term) !u64 {
    const std = @import("std");

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

    const slice = try beam.get([]u64, array, .{.allocator = arena_allocator});

    var total: u64 = 0;

    for (slice) |item| { total += item; }

    return total;
}
"""

test "arena sum" do
  assert 6 == arena_sum([1, 2, 3])
end