課題1: カスタムアロケータ実装
マンダトリー要件 (80点)
Part 1: Stack Allocator実装 (30点)
スタック上に固定サイズのバッファを持ち、LIFOで解放を行うアロケータを実装してください。
要件:
const std = @import("std");
const Allocator = std.mem.Allocator;
pub const StackAllocator = struct {
buffer: []u8,
head: usize,
allocations: std.ArrayList(Allocation),
const Allocation = struct {
offset: usize,
size: usize,
};
pub fn init(buffer: []u8, base_allocator: Allocator) !StackAllocator {
// TODO: 実装
// - bufferをメモリプールとして使用
// - allocationsリストを初期化(base_allocatorを使用)
}
pub fn deinit(self: *StackAllocator) void {
// TODO: 実装
// - allocationsリストを解放
}
pub fn allocator(self: *StackAllocator) Allocator {
// TODO: 実装
// - VTableを設定してAllocatorを返す
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: u8,
ret_addr: usize,
) ?[*]u8 {
// TODO: 実装
// 1. アライメントを計算
// 2. 十分な空き容量をチェック
// 3. allocationを記録
// 4. headを更新
// 5. ポインタを返す
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
// TODO: 実装
// 最後の割り当てのみリサイズ可能にする
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
// TODO: 実装
// LIFO順序での解放をチェック
// 順序違反の場合はパニック
}
};
テストコード:
test "StackAllocator basic operations" {
var buffer: [4096]u8 = undefined;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var stack = try StackAllocator.init(&buffer, gpa.allocator());
defer stack.deinit();
const allocator = stack.allocator();
// 割り当て
const ptr1 = try allocator.alloc(u8, 100);
try std.testing.expect(ptr1.len == 100);
const ptr2 = try allocator.alloc(i32, 50);
try std.testing.expect(ptr2.len == 50);
// LIFO順で解放
allocator.free(ptr2);
allocator.free(ptr1);
}
test "StackAllocator LIFO enforcement" {
var buffer: [4096]u8 = undefined;
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var stack = try StackAllocator.init(&buffer, gpa.allocator());
defer stack.deinit();
const allocator = stack.allocator();
const ptr1 = try allocator.alloc(u8, 100);
const ptr2 = try allocator.alloc(u8, 100);
// 逆順での解放を試みる(パニックするはず)
// このテストは@panicでの失敗を期待
allocator.free(ptr1); // これはptr2の前なのでエラー
allocator.free(ptr2);
}
Part 2: Pool Allocator実装 (30点)
固定サイズのオブジェクトを効率的に管理するプールアロケータを実装してください。
要件:
pub fn PoolAllocator(comptime T: type) type {
return struct {
const Self = @This();
const Node = struct {
next: ?*Node,
data: T,
};
parent_allocator: Allocator,
free_list: ?*Node,
blocks: std.ArrayList([]Node),
block_size: usize,
pub fn init(parent_allocator: Allocator, block_size: usize) Self {
// TODO: 実装
// - block_sizeはブロックあたりのノード数
// - 初期状態ではブロックを割り当てない(遅延割り当て)
}
pub fn deinit(self: *Self) void {
// TODO: 実装
// - 全ブロックを解放
// - blocksリストを解放
}
pub fn allocator(self: *Self) Allocator {
// TODO: 実装
}
fn allocBlock(self: *Self) !void {
// TODO: 実装
// 1. block_size個のNodeを割り当て
// 2. 各Nodeをfree_listに追加
// 3. blocksリストに追加
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: u8,
ret_addr: usize,
) ?[*]u8 {
// TODO: 実装
// 1. サイズが@sizeOf(T)であることを確認
// 2. free_listが空なら新しいブロックを割り当て
// 3. free_listから1つ取り出す
// 4. dataのポインタを返す
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
// プールアロケータはリサイズをサポートしない
return false;
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
// TODO: 実装
// 1. サイズが@sizeOf(T)であることを確認
// 2. bufからNodeのポインタを計算(@offsetOf使用)
// 3. Nodeをfree_listの先頭に追加
}
};
}
テストコード:
test "PoolAllocator performance" {
const Point = struct { x: f32, y: f32, z: f32 };
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var pool = PoolAllocator(Point).init(gpa.allocator(), 64);
defer pool.deinit();
const allocator = pool.allocator();
// 大量の割り当て
var points: [1000]*Point = undefined;
for (&points) |*ptr| {
ptr.* = try allocator.create(Point);
ptr.*.* = .{ .x = 1.0, .y = 2.0, .z = 3.0 };
}
// 解放
for (points) |ptr| {
allocator.destroy(ptr);
}
// 再割り当て(free_listから再利用される)
const new_point = try allocator.create(Point);
allocator.destroy(new_point);
}
Part 3: メモリ使用量トラッキング (20点)
アロケータをラップして、メモリ使用量を追跡するラッパーを実装してください。
要件:
pub const TrackingAllocator = struct {
parent_allocator: Allocator,
total_allocated: usize,
total_freed: usize,
peak_allocated: usize,
allocation_count: usize,
free_count: usize,
pub fn init(parent_allocator: Allocator) TrackingAllocator {
// TODO: 実装
}
pub fn allocator(self: *TrackingAllocator) Allocator {
// TODO: 実装
}
pub fn dumpStats(self: *TrackingAllocator) void {
// TODO: 実装
// 統計情報を標準出力に表示
// - 総割り当て量
// - 総解放量
// - ピークメモリ使用量
// - 割り当て回数
// - 解放回数
// - 現在のメモリ使用量
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: u8,
ret_addr: usize,
) ?[*]u8 {
// TODO: 実装
// 1. 親アロケータで割り当て
// 2. 統計を更新
// 3. ポインタを返す
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
// TODO: 実装
// リサイズ成功時に統計を更新
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
// TODO: 実装
// 1. 統計を更新
// 2. 親アロケータで解放
}
};
テストコード:
test "TrackingAllocator statistics" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var tracking = TrackingAllocator.init(gpa.allocator());
const allocator = tracking.allocator();
// 複数の割り当て
const buf1 = try allocator.alloc(u8, 1000);
const buf2 = try allocator.alloc(i32, 500);
const buf3 = try allocator.alloc(f64, 200);
// 統計を確認
try std.testing.expect(tracking.allocation_count == 3);
try std.testing.expect(tracking.total_allocated > 0);
// 一部解放
allocator.free(buf2);
try std.testing.expect(tracking.free_count == 1);
// 残り解放
allocator.free(buf1);
allocator.free(buf3);
// 最終統計
tracking.dumpStats();
try std.testing.expect(tracking.total_allocated == tracking.total_freed);
}
ボーナス課題 (20点)
Bonus 1: Slab Allocator実装 (10点)
複数のサイズクラスを管理するスラブアロケータを実装してください。
要件:
pub const SlabAllocator = struct {
const SIZE_CLASSES = [_]usize{
16, 32, 64, 128, 256, 512, 1024, 2048, 4096,
};
const Slab = struct {
size_class: usize,
free_list: ?*Node,
memory: []u8,
const Node = struct {
next: ?*Node,
};
// TODO: Slabの実装
};
parent_allocator: Allocator,
slabs: [SIZE_CLASSES.len]std.ArrayList(Slab),
// TODO: SlabAllocatorの実装
// - サイズクラスに基づいて適切なスラブを選択
// - 大きすぎる割り当ては親アロケータに委譲
// - 各スラブでフリーリストを管理
};
パフォーマンス要件:
- 10万回の小さい割り当て(16-256バイト)が1秒以内
- フラグメンテーションを最小化
Bonus 2: Buddy Allocator実装 (10点)
2のべき乗サイズでメモリを管理するバディアロケータを実装してください。
要件:
pub const BuddyAllocator = struct {
const MIN_SIZE = 16;
const MAX_ORDER = 20; // 2^20 = 1MB
const Block = struct {
order: u5,
is_free: bool,
next: ?*Block,
prev: ?*Block,
};
memory: []u8,
free_lists: [MAX_ORDER + 1]?*Block,
// TODO: BuddyAllocatorの実装
// - 割り当て時にブロックを分割
// - 解放時にバディとマージ
// - フラグメンテーションを最小化
};
アルゴリズム:
- 要求サイズに適した最小のorderを計算
- 適切なサイズのフリーブロックを探す
- 大きすぎる場合は再帰的に分割
- 解放時にバディブロックを探してマージ
評価基準
| 項目 | 配点 |
|---|---|
| Part 1: Stack Allocator | 30点 |
| Part 2: Pool Allocator | 30点 |
| Part 3: Tracking Allocator | 20点 |
| **マンダトリー合計** | **80点** |
| Bonus 1: Slab Allocator | 10点 |
| Bonus 2: Buddy Allocator | 10点 |
| **ボーナス合計** | **20点** |
採点詳細
Stack Allocator (30点):
- 正しいalloc実装 (10点)
- LIFOチェック機能 (10点)
- アライメント処理 (5点)
- リサイズサポート (5点)
Pool Allocator (30点):
- 正しいalloc/free (12点)
- ブロック管理 (8点)
- パフォーマンス (5点)
- メモリリークなし (5点)
Tracking Allocator (20点):
- 正しい統計収集 (10点)
- 親アロケータへの委譲 (5点)
- dumpStats実装 (5点)
- マンダトリー: 64点以上(80%)で合格
- ボーナス: 優秀な実装に対する追加評価
- すべてのコードを
exercise01/ディレクトリに配置 - ファイル構成:
合格基準
提出方法
exercise01/
├── stack_allocator.zig
├── pool_allocator.zig
├── tracking_allocator.zig
├── tests.zig
├── bonus1_slab.zig (オプション)
└── bonus2_buddy.zig (オプション)
zig test exercise01/tests.zig
ヒント
Stack Allocator
// アライメント計算
const alignment = @as(usize, 1) << @as(u6, @intCast(ptr_align));
const aligned_offset = std.mem.alignForward(usize, self.head, alignment);
// LIFO検証
if (self.allocations.items.len == 0) return; // エラー
const last_alloc = self.allocations.items[self.allocations.items.len - 1];
if (@intFromPtr(buf.ptr) != @intFromPtr(self.buffer.ptr) + last_alloc.offset) {
@panic("Stack allocator: free must be in LIFO order");
}
Pool Allocator
// Nodeからdataポインタへの変換
const data_ptr = @as([*]u8, @ptrCast(&node.data));
// dataポインタからNodeへの逆変換
const node_ptr = @as(*Node, @ptrFromInt(
@intFromPtr(buf.ptr) - @offsetOf(Node, "data")
));
Tracking Allocator
// ピーク更新
const current = self.total_allocated - self.total_freed;
if (current > self.peak_allocated) {
self.peak_allocated = current;
}
Buddy Allocator
// バディアドレスの計算
fn getBuddy(self: *Self, block: *Block) ?*Block {
const block_addr = @intFromPtr(block);
const memory_start = @intFromPtr(self.memory.ptr);
const offset = block_addr - memory_start;
const size = @as(usize, 1) << @as(u6, block.order);
const buddy_offset = offset ^ size; // XORでバディを計算
if (buddy_offset >= self.memory.len) return null;
return @as(*Block, @ptrFromInt(memory_start + buddy_offset));
}
デバッグのコツ
var gpa = std.heap.GeneralPurposeAllocator(.{
.safety = true,
.verbose_log = true,
}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) std.debug.print("LEAK!\n", .{});
}
- アドレスの可視化:
std.debug.print("Address: 0x{x}, Size: {}, Align: {}\n", .{
@intFromPtr(ptr),
size,
alignment,
});
- 状態のダンプ:
pub fn debugDump(self: *StackAllocator) void {
std.debug.print("=== Stack Allocator ===\n", .{});
std.debug.print("Buffer: 0x{x} - 0x{x}\n", .{
@intFromPtr(self.buffer.ptr),
@intFromPtr(self.buffer.ptr) + self.buffer.len,
});
std.debug.print("Head: {}\n", .{self.head});
std.debug.print("Allocations: {}\n", .{self.allocations.items.len});
}
参考資料
- Zig Standard Library Allocators: https://ziglang.org/documentation/master/std/#A;std:heap
- Memory Pool Pattern: https://gameprogrammingpatterns.com/object-pool.html
- Buddy Memory Allocation: https://en.wikipedia.org/wiki/Buddy_memory_allocation
- Slab Allocation: https://en.wikipedia.org/wiki/Slab_allocation