deinit and arena allocator

Hi,

Brand new to zig(and loving it), coming from a background of memory managed langs (go, java etc.)

I am a bit lost with deinit. I understand that it should do cleanup and free any memory that it allocated.

How does this relate to when the object is instantiated using an arena allocator. From what I can gather, I should not deinit individual items and only deinit the arena allocator.

In my mind this presents two gaps. This would mean that an arena allocator is not a drop in replacement and would require additional changes to code. I’m not saying that this is a problem but naively, I anticipated it to work more polymorphically.

Secondly, this also means that deinit should be used exclusively for deallocation. If it does any other cleanup, it may end up not being called if the object was instantiated with an arena allocator - or deinit needs to be called on it anyway, which would negate the benefit of using the arena allocator (at least for that object).

I have tried to find some conclusive documentation around deinit and what it should do, but could not find any. If there is some documentation somewhere, perhaps that would answer my questions?

I would appreciate any help and explanations.

Thanks in advance,
Shri

1 Like

You should still deinit individual objects. For one, they may be deinitializing resources other than memory (file handlers, network sockets, etc.), and two, frees against an arena allocator are simply noops, and AFAIK completely optimized away at compile time. Adding per-object deinits and frees where applicable only makes your code more portable, should it be used with a different allocator in the future.

Edit: Actually checking out the implementation, it’s not a noop. I think it will actually free the memory so long as it is the most recent allocation that hasn’t been free’d yet.

2 Likes

Welcome to Ziggit @drone-ah! Is that a Mahabharata pun?

There are three deallocations used in the standard library, and all Zig code would do well to follow that example.

free is paired with alloc, creating or releasing a slice of any T. create returns a pointer, and that memory is freed with destroy.

Last is init, which doesn’t have to take an allocator, but will (Chekov’s Gun principle) perform allocation when it does. Anything which is init’ed must be deinit’ed when one is done with it, because the implication is that it controls some resources which have a lifetime, and must be properly disposed of.

If init takes an Allocator then deinit must as well.

A wrinkle is that initialized types don’t necessarily own and control every bit of memory they point to. This needs to be spelled out in the contract for the type, ideally in the doc comment for init. Anything it’s responsible for, deinit disposes.

As for arenas: if code is special to an arena, then mostly you don’t need to deinitialize… mostly, because sometimes there’s finalization work which doesn’t involve freeing memory. To be a part of deinit this to be guaranteed error-free; deinit should always have the return type void, not !void, so it can be used in defer statements.

But for something like a library, it’s good practice to make it allocation-agnostic. An Allocator taken from an ArenaAllocator is almost a no-op in deallocation: it spends a few cycles determining if the memory it’s given is the most-recently-allocated parcel, in which case it can bump a pointer down and reuse it immediately. Otherwise it does nothing.

So it’s always safe to free / destroy memory from an arena, as long as you treat it as freed of course. But it won’t actually be released until the arena is deinit’ed.

1 Like

Omg. I was told this was a no-op. I have a piece of code where this corner case could give a really serious problem. The pointer would be identical to the last value such not triggering a change. I guess I need to make my own allocator. Thanks for the heads up.

Just to be clear, it changes the allocator’s state. Say you alloc 12 u8, then you free it immediately and re-alloc another 12: the second 12 bytes will be the same bytes.

It doesn’t change the pointer handed in, in any way.

Right but in this case I have some code looking at a current pointer from a thread. With some odd timing it could miss the switch to ?null and the new value and never be the wiser.

That may be true, I can’t tell. But I do know that your pointer will have the same value after calling e.g. deinit using an Allocator from an arena. It will still point to the memory and it will be invalid to use it. Specifically it won’t be null’ed out.

The only difference between latest-pointer and any other allocated pointer, is that the arena will make it possible to reallocate the latest-pointer memory. In the other cases nothing will change, although the only sane thing to do is treat the freed pointer as illegitimate.

Unless you’re deeply inspecting the contents of the arena struct, which I advise against, this is unlikely to have an impact on visible behavior of your program.

But in a language like Zig, I can’t be sure of that. If you are, for instance, counting on the integer value of a pointer to consistently increase with allocations, that’s a different kind of mistake, and one of the consequences would in fact be to make this behavior visible.

1 Like

The only thing I was counting on was that the exact pointer wouldn’t be reused in scope of this arena. It was a lazy and convenient way to check if I should even bother grabbing a lock and verify that there was a worker to consume it. Multiple sources had informed me it was a no-op.

It makes a ton of sense to reclaim. Especially since the common alloc defer alloc defer pattern will continuously reclaim multiple chunks of memory. (I assume). It would be a very rare occurrence where the few cycles saved compares to keep reusing the same (hopefully cached) memory.

1 Like

Thank you for your detailed reply.

I’ve had this username for at least 15 years and you are the first person to make the connection. I am impressed - and thank you :heart_eyes:

I’m really happy to hear that - it makes logical sense and is symmetrical.

I’m curious about this one. My naive implementation might have saved the allocator in the init as a field and used that in deinit - saving a parameter being passed in. In hindsight it makes sense to pass in an allocator to both functions.

In theory though, wouldn’t it mean that a different/wrong allocator is passed into deinit? Is the idea that it’s up to the caller to ensure that the allocators are consistent?

1 Like

Thank you for your reply and for looking into and linking the implementation. I appreciate it.

I forget that I can look up the code - though I will admit that I do not fully understand what that code does yet - and will have to spend a bit of time figuring it out.

Ah yes, this would be a genuine problem. Like I said, hard to tell! Rewriting the ArenaAllocator to be a pure bump allocator should be fairly straightforward though.

We call a struct / composite type which carries its own Allocator “managed”. It’s valid, but one should lean away from doing this. My personal criterion is basically this: is the type ‘data’, or is it an ‘actor’ in the broad sense? If it primarily represents behavior, and creates and manages a lot of data, it’s a good place to store an Allocator. If not, not.

Yes, that’s something which user code has to manage. It’s quite practical to get this right, but you’ll see that since Zig requires that your code manage all memory resources, the policy for that will affect everything about the code.

What I mean is that if your code is doing something ‘weird’ enough that it could be unclear which allocator was used for what, that won’t be the only memory bug in the program. Always know where the bytes are!

3 Likes

You can easily make free a no-op:

fn my_free (_: *anyopaque, _: []u8, alignment: Alignment, _: usize) void {}
const my_vtable = blk:{
   // If we could just grab the functions
   // inside the Arena implementation, 
   // we could easily create a new vtable 
   // with free replaced. But these functions
   // are private, so we do a little dance to
   // grab them.
   var arena: std.heap.ArenaAllocator = undefined;
   const allocator = arena.allocator();
   var vtable = allocator.vtable;
   vtable.free = my_free;
   break :blk vtable;
};

fn myArenaAllocator(arena: *std.heap.ArenaAllocator) Allocator{
  var allocator = arena.allocator();
  allocator.vtable = &my_vtable;
  return allocator;
}
3 Likes

Thank you for taking the time to answer my questions. I am now much clearer on these concepts.

1 Like