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