Arena allocators vs. general allocators: is it really binary

yes, it goes beyond single functions.

But functions/data structures/whatever can depend on the strategy, and I think that should clearly be communicated.

gpa signals that it does not depend on a strategy.
arena signals that it does depend on an arena-like strategy, that is, a resettable allocator.
allocator doesnt clearly communicate anything, we just assume it to be analogous to gpa, which I am ok with I just prefer otherwise.

I’ve seen alc: std.mem.Allocator before, and I honestly really like it for the stupidest reason- it makes me think of alchemy.

1 Like

I definitely feel your vibe here. I may have a flaw in my thinking that @vulpesx or another can shed light on… I’ll demonstrate with an example below… but first:

This makes plenty of sense, and may suggest, again, the ā€œgeneralā€ nature of using gpa - ā€œā€¦does not depend on a strategyā€ - I read this to mean: the caller might intend to make use of an arena, but another caller might use an actual PageAllocator or whatever, and the lib can handle either.

So, for instance, isn’t something like this ā€œfineā€?

const Playlist = struct {
   // ...
   pub fn deinit(self: *Playlist, al: Allocator) void {
      self.deque.deinit(al);
   }

   pub fn add(self: *Playlist, al: Allocator, song: Song) Allocator.Error! void {
      self.deque.pushFront(al, song);
      // and do some other stuff with `song` or our own state....
   }
};

// ...

// program 1:
{
   var fba = FixedBufferAllocator.init(&buffer);
   const allocator = fba.allocator();
   var my_playlist: Playlist = .init(...);
   defer my_playlist.deinit(allocator); // note this!
   my_playlist.add(allocator, ...);
   // blah blah
} // my_playlist is explicitly deinitialized; deque will free each alloc for each item that was pushFront()ed (or otherwise added)

// program 2:
{
   var arena = ArenaAllocator.init(std.heap.page_allocator);
   defer arena.deinit();
   const allocator = arena.allocator();
   // ...
   {
      var my_playlist: Playlist = .init(...);
      // note NO my_playlist.deinit() here, intentionally
      my_playlist.add(allocator, ...);
      // blah blah      
   }
   // some strategic place along the line...
   arena.reset(.retain_capacity); // resets playlists (including my_playlist) and anything else built with the arena, all at once, and quickly

   {
      // some other stuff... another turn of the grist mill, or whatever
   }
}

my_playlist.deinit(al) (and thus deque.deinit(al)) were never explicitly called in program 2 because program 2’s strategy was different, and program 1 and program 2 were each happy that they could use their own strategy because Playlist (and Deque, below) are both versatile enough for this.

Perhaps there’s nothing wrong with this code (forgive - I freehanded the code, and I’m still a little mistake-prone), and, at the end of it all, all a pro has to say is: ā€œyeah, fine, but then the convention is to call that thing gpa, not allocator or, worse yet, al (who would use such a stupid name?!). Then, my only comment would be: ā€œfine, but some will perhaps think that gpa might imply that GeneralPurposeAllocator should be used, or that, at least, deinit() should be called to clean-up, because … you wouldn’t want to use an arena, would you?ā€ I can certainly see where the confusion could come in.

Edit: my_playlist might have more reasonably been declared outside of my little scope curleys… but you get the idea.

Haven’t caught up with the entire discussion, so apologies if I am duplicating something already said, but I want to mention that just ā€œarenaā€ itself is not unary, as there are at least three different semantics that can be powered by arena:

  • A short-lived scratch space storing intermediate results in a function, which is reset before/just after the function returns
  • Long-lived ā€œsubsystemā€ arena which is used to allocate return values for the caller
  • A permanent arena which is never freed
5 Likes