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);
}
};