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.)
-
is this a reasonable endeavor?
-
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!