View Source Allocation using zigler
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.
Raw Beam Allocator
The first allocator is raw_allocator
. This allocator wraps the
nif allocator
provided by the BEAM. You should 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(env: beam.env, count: usize) !beam.term {
var slice = try beam.raw_allocator.alloc(u16, count);
defer beam.raw_allocator.free(slice);
for (slice, 0..) |*entry, index| {
entry.* = @intCast(index);
}
return beam.make(env, slice, .{});
}
"""
test "raw allocator" do
assert [0, 1, 2] = allocate_raw(3)
end
raw allocator limitations
because the raw 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.raw_allocator.alloc(u8, 1_000_000);
}
pub fn zigler_free() void {
beam.raw_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
Large Allocator
Zigler provides a large allocator
which allows you to allocate memory ranges
that have a higher alignment than the maximum alignment for builtin types.
Note that using this allocator comes with a memory penalty.
~Z"""
pub fn allocate_large_aligned(count: usize) !usize {
const page = try beam.large_allocator.allocWithOptions(u8, count, 4096, null);
defer beam.large_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 general purpose allocator is accessible using
beam.allocator_.general_purpose_allocator_instance
~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
assert leaks()
refute noleak()
end
beam.allocator
Zigler provides a threadlocal
variable: beam.allocator
. This is set on entry
into the nif and defaults to beam.raw_allocator
~Z"""
pub fn basic(env: beam.env) !beam.term {
const slice = try beam.allocator.alloc(u16, 4);
defer beam.allocator.free(slice);
for (slice, 0..) |*item, index| {
item.* = @intCast(index);
}
return beam.make(env, slice, .{});
}
"""
test "leak checks with allocator" do
assert [0, 1, 2, 3] = basic()
end
Raw nifs
For raw nifs,
beam.allocator
is not set, and may retain a value from an arbitrary previous nif invocation. Consider usage ofbeam.allocator
in a raw nif to be undefined unless it is set in the raw nif.
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"""
const std = @import("std");
pub fn with_arena(env: beam.env) !beam.term {
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(env, slice, .{});
}
"""
test "arena allocator" do
assert [0, 1, 2, 3] == with_arena()
end