Zig NEWS

Cover image for On Allocator Names
Loris Cro
Loris Cro

Posted on

On Allocator Names

Recently I decided to stop awaiting for async to return (blocked on async, the irony) and resumed development of Bork, my Twitch chat client for livecoding.

https://github.com/kristoff-it/bork

That made me look at 2 years old code and the biggest thing that I experienced was annoyance at how I kept calling my allocators.

To make a long story short, here's my advice: in code that is not meant to be a fully reusable library (eg in application code) don't call your allocators allocator (or other contractions like alloc or ally) and instead name them gpa, arena, fba, so that you can better convey their intended usage pattern.

Additionally, don't shy away from passing around two allocators at a time, if it makes sense:

pub fn doSomething(gpa: std.mem.Allocator, arena: std.mem.Allocator) Result {}
Enter fullscreen mode Exit fullscreen mode

I don't have yet a concrete example to show in Bork because I haven't yet fully designed how it should free memory. For now Bork keeps the full list of messages in memory until the application exits, but I want to move eventually to a model where the user configures a max amount of memory that can be used to store message history and we automatically evict old messages once we fill that memory.

In that scenario I will have some kind of ring buffer allocator for storing data relative to one specific message, and another one for data that should never be evicted, like emote images, for example.

In conclusion

Application code doesn't normally need to be allocator agnostic and so by giving concrete names to your allocator interfaces you can gain more clarity and unlock the concept of having multiple allocators at hand at once.

Go from this:

var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
const alloc = gpa.allocator();

var arena = std.heap.ArenaAllocator.init(alloc);
const alloc1 = arena.allocator();
Enter fullscreen mode Exit fullscreen mode

To this:

var gpa_impl: std.heap.GeneralPurposeAllocator(.{}) = .{};
const gpa = gpa_impl.allocator();

var arena_impl = std.heap.ArenaAllocator.init(gpa);
const arena = arena_impl.allocator();
Enter fullscreen mode Exit fullscreen mode

And same with functions that accept allocators:

fn foo(gpa: std.mem.Allocator) !Result {}
fn bar(arena: std.mem.Allocator) !Result {}
fn baz(gpa: std.mem.Allocator, arena: std.mem.Allocator) !Result {}
Enter fullscreen mode Exit fullscreen mode

Top comments (9)

Collapse
 
msw profile image
Martin

I don't get the non-typed approach either. If you want an arena allocator, why wouldn't you expect a std.heap.Allocator? If you get allocators passed in, it's the caller who determines the strategy. Naming doesn't enforce the correct choreography. This, together with signaling of memory ownership (i.e., e.g., need to pass a copy of keys for allocated keys in a hashmap) is still an area of question marks for me.

Collapse
 
kristoff profile image
Loris Cro

Because "arena" and "gpa" are usage patterns, and not unique implementations. Both std.heap.GeneralPurposeAllocator and malloc are "gpa"s, and likewise arenas can be implemented in different ways, like FixedBufferAllocator for example.

Collapse
 
adjectiveallison profile image
Allison Durham

Is this just a current quirk of zig and the way the Allocator interface is implemented?

Collapse
 
msw profile image
Martin

It would be nice to have a vision laid out by andrew on how he imagines these(*) things for 0.12 (the "stable" 0) / 1.0, if he has one. I'm not aware of such a vision documented, if you ("whoever") have an article/video to share, I'm curious to get to know it, please share.

(*) I see accepting allocators as tightly scoped strategy pattern, which would mean it's the caller who determines the strategy.
If there's requirements to use a specific strategy (but via an object that the caller passes in, manages and owns), I would expect/wish/hope for a strong reflection of that fact in the fn signature, and a way for both author and user to make sure a compatible implementation has been passed at compile time. I would also assume the names (in the example with multiple allocators) would communicate their use, instead of the author's preferred strategy, e.g. communicating whether an allocator is used for few, big allocations with long lifetime or recurring small allocations of samely typed/sized things with high frequency and churn. Obviously the interface can only take so much, and there's need for documentation as well IMO...

Collapse
 
cancername profile image
cancername • Edited

I'd like to propose _state instead of _impl in order to better convey that it is storing data the allocator works with, not just a container for some functions.

Re: the last example, this seems as though it defeats the purpose of having generic allocators in the first place. Perhaps it would be better to explain the usage of each allocator in a doc comment instead of suggesting a specific implementation. This might just be a difference in philosophy though, I generally enjoy writing code as though it were library code.

Collapse
 
voilaneighbor profile image
Fifnmar

You are definitely not the first one to come up with the idea that even at the usage site there are two types of allocations. I remember reading someone's C blog saying that we should use an additional allocator and name it scratch. I can't remember who.

I would suggest that instead of naming the allocators after their concrete types, you just use at most two types of allocators:

const Allocator = std.mem.Allocator;

fn my_fn(my_arg: Arg, allocator: Allocator, scratch: Allocator) {
    //...
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
flipbit profile image
Max

I like it :)
Passing two allocators to a function with different usage in mind looks quite a bit like Odin's context system.
It's also very similar to what Casey Muratori did in his Handmade Hero series with one arena for permanent data and one for temporary data that gets freed after a frame.

I'm currently using Odin (originally I wanted to use Zig but I found that it's still too early) and the context system does annoy me quite a bit since opting out is theoretically possible but impractical for general usage because the standard library relies on it. It's also just a little more magic than I'm happy with in a low level language. So the explicit style suggested here is pretty much the sweet spot for me.

Collapse
 
andres profile image
Andres

My gut tells me that name based typing is not great. I think when you specifically need an Arena or a GPA what you really want to encode is who (caller or callee) is responsible to free (owns) the allocated data. Almost every time you use an Arena, you are implying that the caller owns the data.

Collapse
 
flipbit profile image
Max

Personally I think it's fine if you use naming to convey intended usage in your own application codebase.

I agree with you if you mean that this convention could get tricky if you have a team and some people coming and going. In this case though, you don't have to be super academic and dogmatic about everything. You want your little tool to be useful in a reasonable timeframe. It's fine to cut some corners and Loris has also given the caveat "in code that is not meant to be a fully reusable library", which you and I might want to expand a little but that's not a reason to dismiss the idea entirely.