Composite allocator - "stack first... then arena once stack is full...."

I wanted to try my hand at my first custom composite allocator - not really “custom” because it’s not very primitive - it just composes a couple of ready-made allocators to serve this purpose.

Purpose? - Start with a FixedBufferAllocator sized according to a reasonable guess; then, when out of space, use a GPA in an Arena. As usual with an arena, when finished, free all (the heap allocations) at once.

This would be used in a serial workflow where the data structure that is to be filled is often going to “fit” in the FixedBuffer, but, for those few cases where it doesn’t, the heap will be handy, and, once the data structure is filled, it’ll be serialized to the wire, to the hinterlands, and will no longer be needed, so can be released and redeemed for the next iteration. (In case a “picture” helps imagine this.)

  1. is this a reasonable endeavor?

  2. is this a reaonable way to do this? (I realize it’s a blunt way, not a very clever way… but, to get the gist across….)

pub const StackFirstArenaAllocator = struct {
   fixed: FixedBufferAllocator,
   arena: ArenaAllocator,
   primary: bool = true,

   pub fn init(buffer: []u8, child_allocator: Allocator) StackFirstArenaAllocator {
      return .{
         .fixed = .init(buffer),
         .arena = .init(child_allocator),
      };
   }

   pub fn deinit(self: StackFirstArenaAllocator) void {
      self.arena.deinit();
   }

   pub fn allocator(self: *StackFirstArenaAllocator) Allocator {
      return .{
         .ptr = self,
         .vtable = &.{
               .alloc = alloc,
               .resize = resize,
               .remap = remap,
               .free = free,
         },
      };
   }

   pub fn alloc(ctx: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 {
      const self: *StackFirstArenaAllocator = @ptrCast(@alignCast(ctx));
      if (self.primary) {
         const fixed = self.fixed.alloc(n, alignment, ra);
         if (fixed) return fixed; // else...
      }
      self.primary = false; // switch to using arena
      return self.arena.alloc(n, alignment, ra);
   }

   pub fn resize(ctx: *anyopaque, buf: []u8, alignment: mem.Alignment, new_size: usize, return_address: usize) bool {
      const self: *StackFirstArenaAllocator = @ptrCast(@alignCast(ctx));
      return if (self.primary) self.fixed.resize(buf, alignment, new_size, return_address)
                          else self.arena.resize(buf, alignment, new_size, return_address);
   }

   pub fn remap(ctx: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 {
      const self: *StackFirstArenaAllocator = @ptrCast(@alignCast(ctx));
      return if (self.primary) self.fixed.remap(memory, alignment, new_len, return_address)
                          else self.arena.remap(memory, alignment, new_len, return_address);
   }

   pub fn free(ctx: *anyopaque, buf: []u8, alignment: mem.Alignment, return_address: usize) void {
      const self: *StackFirstArenaAllocator = @ptrCast(@alignCast(ctx));
      return if (self.primary) self.fixed.free(buf, alignment, return_address)
                          else self.arena.free(buf, alignment, return_address);
   }

   pub fn reset(self: *StackFirstArenaAllocator, mode: ArenaAllocator.ResetMode) bool {
      self.fixed.reset();
      if (!self.primary) {
         self.primary = true;
         return self.arena.reset(mode);
      }
      return true;
   }
};

(As an aside, is it weird that the FixedBufferAllocator is in std.heap even though it’s stack-based? Probably that’s a whole thread somewhere, so just punt me if you want.)

(As another aside, comment on an alternative to the normal FixedBufferAllocator pattern of providing the buffer – rather than providing a buffer, might the (.init()) interface just ask for a comptime usize, and then house the buffer of that comptime usize within the struct (StackFirstArenaAllocator, here), rather than asking it to live in the calling code?)

Thanks!

It’s not uncommon. In fact, it’s already in the standard library as StackFallbackAllocator, which attempts to use a FixedBufferAllocator first and falls back to a user-provided allocator if the FBA returns error.OutOfMemory. It accepts any fallback allocator, but if provided with an Arena it’s equivalent to yours (and the implementation is pretty similar, although it does use FixedBufferAllocator’s ownsPtr(*anyopaque) bool method to be a bit smarter about free and remap.

2 Likes

OOPS! :face_with_peeking_eye:

(well, glad the idea wasn’t bone-headed, at least.)

1 Like

Certaintly it is. I see that you’ve been introduced to StackFallbackAllocator already, this observation might be worth making anyway:

This asks itself if it’s switched from stack allocation to arena allocation, but what it wants to ask is if the memory pointer comes from the stack or not. StackFallbackAllocator uses a function ownsPtr which you can find somewhere in here.

Yes, thank you - I noticed this and some other shortcomings of my first-stab attempt once I had StackFallbackAllocator code to look at. My thought was definitely conceptual and infantile… and ultimately needless!

However, it did expose a few tangents that are interesting. I think it’s more proper to fork those into separate topic posts, so I’ll do that some day, and call this one closed.

1 Like