zig-alloc - 解説

実装の詳細

Zigのアロケータインターフェース

Zigの std.mem.Allocator は以下の構造:

pub const Allocator = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub const VTable = struct {
        alloc: *const fn (*anyopaque, usize, u8, usize) ?[*]u8,
        resize: *const fn (*anyopaque, []u8, u8, usize, usize) bool,
        free: *const fn (*anyopaque, []u8, u8, usize) void,
    };
};

アライメントの処理

// ptr_align は log2 で渡される
// 例: ptr_align = 3 → alignment = 8
const alignment = @as(usize, 1) << @intCast(ptr_align);

// アドレスをアライメントに合わせる
const aligned_index = std.mem.alignForward(usize, current_index, alignment);

FixedBufferAllocator の動作

初期状態:
buffer: [------------------------------------]
         ^end_index=0

100バイト割り当て後:
buffer: [####################################]
              ^end_index=100

200バイト割り当て後(アライメント調整込み):
buffer: [####################################]
                            ^end_index=308

reset後:
buffer: [------------------------------------]
         ^end_index=0

よくある間違い

1. アライメントの無視

// 間違い: アライメントを無視
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
    const self = @ptrCast(*Self, ctx);
    const result = self.buffer.ptr + self.end_index;
    self.end_index += len;
    return result;  // アライメントが守られていない!
}

// 正しい: アライメント調整
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
    const self = @ptrCast(*Self, ctx);
    const alignment = @as(usize, 1) << @intCast(ptr_align);
    const aligned = std.mem.alignForward(usize, self.end_index, alignment);
    // ...
}

2. オーバーフローチェックの欠如

// 間違い: オーバーフローチェックなし
if (self.end_index + len > self.buffer.len) {
    return null;
}

// 正しい: アライメント後のインデックスでチェック
const aligned_index = std.mem.alignForward(usize, self.end_index, alignment);
if (aligned_index + len > self.buffer.len) {
    return null;
}

3. vtable の寿命問題

// 間違い: ローカル変数のアドレスを返す
pub fn allocator(self: *Self) std.mem.Allocator {
    const vtable = VTable{ ... };  // スタック上
    return .{ .ptr = self, .vtable = &vtable };  // ダングリングポインタ!
}

// 正しい: コンパイル時定数を使用
pub fn allocator(self: *Self) std.mem.Allocator {
    return .{
        .ptr = self,
        .vtable = &.{  // コンパイル時定数
            .alloc = alloc,
            .resize = resize,
            .free = free,
        },
    };
}

パフォーマンス考慮事項

アロケーション速度比較

アロケータ alloc free 特徴
FixedBuffer O(1) O(1) 最速、サイズ固定
Arena O(1)* O(1) 高速、一括解放
General O(n) O(n) 柔軟、オーバーヘッド

*新規バッファ確保時を除く

キャッシュ効率

// アリーナは連続したメモリを確保
// → キャッシュヒット率が高い

var arena = ArenaAllocator.init(page_allocator);
const items = try arena.allocator().alloc(Item, 1000);
// items は連続したメモリ領域に配置される

応用パターン

スクラッチアロケータ

// 一時的な計算に使用
pub fn doComputation(permanent_alloc: std.mem.Allocator) !Result {
    var scratch = ArenaAllocator.init(permanent_alloc);
    defer scratch.deinit();

    const temp_data = try scratch.allocator().alloc(f64, 10000);
    // 計算...

    // 結果は永続アロケータで確保
    const result = try permanent_alloc.create(Result);
    result.* = computed_result;
    return result;
}

フォールバックアロケータ

pub const FallbackAllocator = struct {
    primary: std.mem.Allocator,
    fallback: std.mem.Allocator,

    fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
        const self = @ptrCast(*FallbackAllocator, ctx);
        return self.primary.rawAlloc(len, ptr_align, ret_addr) orelse
               self.fallback.rawAlloc(len, ptr_align, ret_addr);
    }
};