Revisiting alignedAlloc and alignedFree

In the process of writing an answer for this topic about free with alignment, I was revisiting this older topic and trying to adapt @kALLEBALIK’s solution to recent Zig:

And here is the variation I created (based on both topics):

const std = @import("std");
const builtin = @import("builtin");

pub fn aligned(alignment: u29) std.math.IntFittingRange(0, std.math.log2(std.heap.page_size_max)) {
    std.debug.assert(std.mem.isValidAlign(alignment));
    std.debug.assert(alignment > 0);
    std.debug.assert(alignment <= std.heap.page_size_max);
    return @intCast(std.math.log2(alignment));
}

pub fn alignedAlloc(allocator: std.mem.Allocator, T: type, alignment: u29, n: usize) ![]u8 {
    const last = comptime aligned(std.heap.page_size_max);
    switch (aligned(alignment)) {
        inline 0...last => |a| {
            return try allocator.alignedAlloc(T, 1 << a, n);
        },
        else => unreachable,
    }
}

pub fn alignedFree(allocator: std.mem.Allocator, alignment: u29, memory: anytype) void {
    const last = comptime aligned(std.heap.page_size_max);
    switch (aligned(alignment)) {
        inline 0...last => |a| {
            const T = @typeInfo(@TypeOf(memory)).pointer.child;
            return allocator.free(@as([]align(1 << a) T, @alignCast(memory)));
        },
        else => unreachable,
    }
}

pub inline fn pageAlign() u29 {
    return @intCast(std.heap.pageSize());
}

// pub const std_options: std.Options = .{
//     .page_size_max = 65536,
// };

const MyStruct = struct {
    allocator: std.mem.Allocator,
    bytes: []u8,

    pub fn init(allocator: std.mem.Allocator, bytes: []const u8) !@This() {
        const buffer = try alignedAlloc(allocator, u8, pageAlign(), bytes.len);
        @memcpy(buffer, bytes);
        return .{
            .allocator = allocator,
            .bytes = buffer,
        };
    }

    pub fn deinit(self: *@This()) void {
        alignedFree(self.allocator, pageAlign(), self.bytes);
    }
};

export fn example() [*]u8 {
    const allocator = std.heap.page_allocator;

    var myStruct = MyStruct.init(allocator, &[_]u8{ 1, 2, 3, 4 }) catch unreachable;
    defer myStruct.deinit();
    return myStruct.bytes.ptr;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var myStruct = try MyStruct.init(allocator, &[_]u8{ 1, 2, 3, 4 });
    defer myStruct.deinit();
}

I tried to compare it a bit with the godbolt output of the original, but I don’t have enough assembly experience, to be able to tell whether the code produced by my version is good.

On a high level it seems good to me that my version directly switches on the powers of 2, but I don’t know whether that matters in a practical way. (Quite a few versions seem to result in similar code being generated and it is easy to get lost between different targets and build modes)

Any ideas about changing the code further?