DebugAllocator.deinit does not free the memory?

I’m using Zig 0.15.2.

I assumed that calling DebugAllocator.deinit frees all memory in it (independent from all the leak checking stuff). That doesn’t seem to be the case though or am I misunderstanding this somehow? Here is my snippet:

pub fn main() !void {
    const allocator_type: enum { debug, arena } = .debug;
    var outer: std.heap.DebugAllocator(.{}) = .{};
    var inner = switch (allocator_type) {
        .debug => std.heap.DebugAllocator(.{ .safety = false }){ .backing_allocator = outer.allocator() },
        .arena => std.heap.ArenaAllocator.init(outer.allocator()),
    };
    _ = try inner.allocator().alloc(i64, 100);
    _ = inner.deinit();
    _ = outer.deinit();
}

Since the inner allocator has safety = false, I expected this to run through without any output. The call to outer.deinit reports a leak though. And that’s after I called inner.deinit. Just to prove that I didn’t miss something very obvious, I added the switch to use an arena as a drop-in replacement of inner. That works so it must be in the DebugAllocator. I didn’t dig very deep yet, but the code of deinit looks good enough to me.

pub fn deinit(self: *Self) std.heap.Check {
    const leaks = if (config.safety) self.detectLeaks() else false;
    if (config.retain_metadata) self.freeRetainedMetadata();
    self.large_allocations.deinit(self.backing_allocator);
    self.* = undefined;
    return if (leaks) .leak else .ok;
}

Basically, the explicit purpose of DebugAllocator is to report leaks, not handle leaks.
It doesn’t promise that it auto-frees all memory on deinit, because that’s the purpose of an arena.
So you are correct and DebugAllocator.deint() does not free the memory you allocated with it.

2 Likes

Yeah DebugAllocator actually doesn’t seem to free much on .deinit(), and I don’t think it’s meant to. If you look at the code, the only thing it actually does in your case is that it frees its large_allocations hash table, which seems to contain some metadata and pointers to buffers. But it doesn’t go any deeper than that. The buffers themselves are not touched.

I think generally it’s assumed that you only have a single “global” DebugAllocator (or in fact SmpAllocator, c_allocator etc.) for the runtime of your application, and then for “inner” scoped allocators you use ArenaAllocators, which do that full-sweep free on .deinit().

1 Like

Docu says:

pub fn deinit(self: *Self) std.heap.Check

    Returns std.heap.Check.leak if there were leaks; std.heap.Check.ok otherwise.
1 Like

Thanks to you all! I think the deallocation of the large_allocations threw me off. I thought that those were basically the internal pages of the allocator and if those are gone, that’s it. And I didn’t see the absence of evidence in the docs as evidence of absence of behavior :grin:

1 Like