Alright, the topic is avoiding allocations using small buffer optimization. This is also helpful for storing generic data for polymorphic types.
This is commonly used with strings and the typical pattern is something like a union with an array the size of a slice (2 * sizeof(usize))
and if the number of characters goes over that, it allocates and sets a flag letting you know which one is in use.
I’m going to kick this off with a video from Jason Turner: https://www.youtube.com/watch?v=CIB_khrNPSU
I bet we could probably be more clever than that and do something more interesting/optimal.
Here’s a basic buffer that I hacked together a while ago. It’s very situationally dependent and uses an extern union so I wouldn’t say it’s a good “generic” buffer, but it suited the need I had at the time:
const ClosureBuffer = struct {
const BufferSize = @sizeOf(usize) * 6; // or whatever size...
items: extern union {
inplace: [BufferSize]u8 align(@alignOf(usize)),
any_ptr: *align(@alignOf(usize)) anyopaque,
},
state: enum { buf, ptr },
pub fn init(x: anytype) ClosureBuffer {
const T = @TypeOf(x);
var self: ClosureBuffer = undefined;
if (@sizeOf(T) <= BufferSize) {
const tmp: *T align(@alignOf(usize)) = @ptrCast(@alignCast(&self.items.inplace));
tmp.* = x;
self.state = .buf;
} else {
const ptr = std.heap.c_allocator.create(T) catch @panic("Closure.init: Out of memory.");
ptr.* = x;
self.items.any_ptr = ptr;
self.state = .ptr;
}
return self;
}
pub fn cast(self: *ClosureBuffer, comptime T: type) *T {
return switch (self.state) {
.buf => @ptrCast(@alignCast(&self.items.inplace)),
.ptr => @ptrCast(@alignCast(self.items.any_ptr)),
};
}
pub fn deinit(self: *ClosureBuffer, comptime T: type) void {
if (self.state == .ptr) {
std.heap.c_allocator.destroy(@as(*T, @ptrCast(@alignCast(self.items.any_ptr))));
}
}
};
There’s a few things that could make this more generic - instead of a *anyopaque
, we could use []u8
and use something like rawFree
instead of having to cast back to the type to destroy it as this is what happens in the destroy
call for allocators anyway:
pub fn destroy(self: Allocator, ptr: anytype) void {
const info = @typeInfo(@TypeOf(ptr)).Pointer;
if (info.size != .One) @compileError("ptr must be a single item pointer");
const T = info.child;
if (@sizeOf(T) == 0) return;
const non_const_ptr = @as([*]u8, @ptrCast(@constCast(ptr)));
self.rawFree(non_const_ptr[0..@sizeOf(T)], log2a(info.alignment), @returnAddress());
}
But I want to hear more ideas about what else we can do to make this better. I think having a nice small buffer that can be used in place of direct pointer or slice would be a good addition to the ecosystem.