I'm having trouble with memory management in Zig (using v0.13.0), regarding global and static local variables deinitialization.
I've written the following program, but memory leaks, and thus the program panics when enabling safety for the GeneralPurposeAllocator. I've read the documentation, especially the part regarding static local variables, but it doesn't mention (or it's in another section and I've missed it) anything on how to deinit the memory.
const std = @import("std");
const key_t = u32;
const value_t = u128;
const computed_map_t = std.AutoHashMap(key_t, value_t);
var default_gpa: std.heap.GeneralPurposeAllocator(.{}) = undefined;
const default_allocator = default_gpa.allocator();
pub fn fibonacci(value: key_t) value_t {
const _state = struct {
// How do I deinit this map on program end ?
// Or is it possible to mark it as "ok to leak" ?
var previously_computed = computed_map_t.init(default_allocator);
};
if (value == 0) return 0;
if (value == 1 or value == 2) return 1;
// Fallback "dumb" implementation because HashMap cannot be used in comptime
if (@inComptime()) {
return fibonacci(value - 2) + fibonacci(value - 1);
} else {
if (_state.previously_computed.get(value)) |res| {
return res;
} else {
const res = fibonacci(value - 2) + fibonacci(value - 1);
_state.previously_computed.put(value, res) catch {
@panic("Something went wrong");
};
return res;
}
}
}
pub fn main() !void {
default_gpa = @TypeOf(default_gpa){};
defer {
if (.leak == default_gpa.deinit()) {
@panic("GPA leaked !");
}
}
const value = 5;
const result = fibonacci(value);
std.debug.print("fibonacci({d}) = {d}\n", .{ value, result });
}
I've thought about initializing the map in the main function and passing it to the fibonacci
function as a pointer, but it kinda defeats the point (this map is only useful in the function, so I'd like to keep the scope of this variable restrained to the function scope).
Is there something I missed? Alternativly, do you have any "hacks" on how to achieve this in a clean way?
With the way you have it setup, it will always show as a leak, because there is no way to get a ref to deinit the map since you've hidden it.
I think a better option would be to init the map and access it from the function rather than pulling in a ref to default_allocator.
var fib_map: computed_map_t = undefined;
pub fn fib(key: key_t) value_t {
const _state = struct {
previously_computed = fib_map,
}
}
pub fn main() void {
fib_map = computed_map_t.init(allocator);
defer fib_map.deinit();
_ = fib(5);
...
}
And generally speaking, zig will make your life difficult if you try to hide allocations in this way. A core part of the philosophy of the lang is to prevent this style of ghost allocations.
Personally I would go with a struct that owns the map and has a function that does the map lookup and otherwise delegates to the full impl function if none matches. This way it also becomes something you can generic.
pub fn MapMemoizer(comptime K: type, comptime V: type, comptime implFn: fn(key: K) V) type {
return struct {
allocator: Allocator,
map: Map = undefined,
implFn: fn(key: K) V = implFn,
Self = @This();
const Map = std.AutoHashMap(K, V);
pub fn init(allocator: Allocator, realFn: fn(key: K) V) !Self {
return .{
.allocator = allocator,
.map = try Map.init(allocator),
.realFn = realFn,
};
}
pub fn deinit(self: *Self) void {
self.map.deinit();
}
pub fn call(self: *Self, key: K) V {
if (self.map.get(key)) |value| {
return value;
} else {
const value = self.realFn(key);
self.map.put(key, value);
}
}
}
}
pub fn fibonacci(key: key_t) value_t {
// fib stuff..
}
const FibMemoizer = MapMemoizer(key_t, value_t, fibonacci);
pub fn main() !void {
var fib = try FibMemoizer.init(allocator);
defer fib.deinit();
const result = fib.call(5);
}
You'll notice this init/deinit combo used very heavily in std, and I imagine as zig native libs pop up they will make use of this too for general purpose APIs.