第2章: Arenaアロケータの深層
学習目標
この章を終えると、以下ができるようになります:
- Arenaアロケータの内部実装を完全に理解する
- 様々なArenaパターンを実装できる
- パフォーマンス特性を分析し、最適化できる
- 実世界のアプリケーションで適切に使用できる
Arenaアロケータの設計哲学
メモリ管理の本質的な問題
プログラムにおけるメモリ管理の根本的な課題は、ライフタイムの管理です:
問題: いつメモリを解放すべきか?
C/C++の解決策:
- 手動管理(malloc/free)
- 問題: ダングリングポインタ、二重解放、メモリリーク
Java/Go/Pythonの解決策:
- ガベージコレクション
- 問題: STWパウス、予測不可能性
Rustの解決策:
- 所有権システム
- 問題: 学習曲線、借用チェッカーとの戦い
Zigの解決策:
- 明示的アロケータ + Arenaパターン
- 利点: 明示的、高速、予測可能
Arenaパターンの洞察
Arenaアロケータは、ライフタイムのグループ化という洞察に基づいています:
洞察:
多くのデータは「同じタイミング」で不要になる
例1: Webリクエスト
リクエストデータ、パースされたJSON、レスポンスバッファ
→ すべてリクエスト終了時に不要
例2: コンパイラのAST
トークン、ASTノード、型情報
→ すべてコンパイル完了時に不要
例3: ゲームフレーム
レンダリングコマンド、パーティクル、デバッグ情報
→ すべて次フレームで不要
この洞察により:
// 従来: N回の個別解放
for (items) |item| {
allocator.free(item); // N回のシステムコール
}
// Arena: 1回の一括解放
arena.deinit(); // 1回で全て解放
標準ライブラリのArenaAllocator実装
ソースコード解析
Zigの標準ライブラリのArenaAllocatorを詳しく見ていきます:
const std = @import("std");
// std.heap.ArenaAllocator の簡略版
pub const ArenaAllocator = struct {
child_allocator: std.mem.Allocator,
state: State,
const State = struct {
buffer_list: std.SinglyLinkedList(usize),
end_index: usize,
const BufNode = std.SinglyLinkedList(usize).Node;
fn alloc(self: *State, child_allocator: std.mem.Allocator, len: usize, ptr_align: u8) ![]u8 {
// 実装の詳細...
}
};
pub fn init(child_allocator: std.mem.Allocator) ArenaAllocator {
return .{
.child_allocator = child_allocator,
.state = .{
.buffer_list = .{},
.end_index = 0,
},
};
}
pub fn deinit(self: ArenaAllocator) void {
// バッファリストを走査して全解放
var it = self.state.buffer_list.first;
while (it) |node| {
const next = node.next;
const buf = @as([*]u8, @ptrCast(node))[0..node.data];
self.child_allocator.free(buf);
it = next;
}
}
pub fn allocator(self: *ArenaAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
// ...実装の続き
};
バッファ成長戦略
標準ライブラリの成長戦略を分析します:
const std = @import("std");
pub const GrowthStrategy = struct {
/// 初期バッファサイズ
const INITIAL_SIZE = 4 * 1024; // 4KB
/// 成長率(2倍)
const GROWTH_FACTOR = 2;
/// 最大単一バッファサイズ
const MAX_BUFFER_SIZE = 16 * 1024 * 1024; // 16MB
pub fn nextBufferSize(current_size: usize, required: usize) usize {
if (current_size == 0) {
return @max(INITIAL_SIZE, required);
}
// 2倍に成長、ただし上限あり
var new_size = current_size * GROWTH_FACTOR;
new_size = @min(new_size, MAX_BUFFER_SIZE);
// 要求サイズより大きいことを保証
return @max(new_size, required + 256); // パディング追加
}
};
// 成長パターンのシミュレーション
pub fn simulateGrowth() void {
var size: usize = 0;
std.debug.print("Buffer Growth Pattern:\n", .{});
for (0..10) |i| {
size = GrowthStrategy.nextBufferSize(size, 100);
std.debug.print(" Buffer {}: {} KB\n", .{ i, size / 1024 });
}
}
// 出力:
// Buffer 0: 4 KB
// Buffer 1: 8 KB
// Buffer 2: 16 KB
// Buffer 3: 32 KB
// Buffer 4: 64 KB
// Buffer 5: 128 KB
// Buffer 6: 256 KB
// Buffer 7: 512 KB
// Buffer 8: 1024 KB
// Buffer 9: 2048 KB
メモリレイアウトの最適化
SinglyLinkedList による管理:
メモリレイアウト:
+------------------+ +------------------+ +------------------+
| BufNode | --> | BufNode | --> | BufNode |
| - next: *Node | | - next: *Node | | - next: null |
| - data: usize | | - data: usize | | - data: usize |
+------------------+ +------------------+ +------------------+
| [実際のバッファ] | | [実際のバッファ] | | [実際のバッファ] |
| 4KB | | 8KB | | 16KB |
+------------------+ +------------------+ +------------------+
利点:
1. ノード自体がバッファの一部
→ メタデータのオーバーヘッド最小
2. シンプルなリンクリスト
→ 実装が単純
3. 順方向走査のみ
→ キャッシュ効率が良い
実装例:
const std = @import("std");
pub const OptimizedArena = struct {
const BufNode = struct {
next: ?*BufNode,
capacity: usize,
used: usize,
fn getData(self: *BufNode) []u8 {
// ノードの後ろに実際のデータがある
const node_size = @sizeOf(BufNode);
const ptr = @as([*]u8, @ptrCast(self)) + node_size;
return ptr[0..self.capacity];
}
fn allocFromBuffer(
self: *BufNode,
len: usize,
alignment: usize,
) ?[]u8 {
const data = self.getData();
const aligned_index = std.mem.alignForward(usize, self.used, alignment);
if (aligned_index + len > self.capacity) {
return null; // バッファ不足
}
const result = data[aligned_index .. aligned_index + len];
self.used = aligned_index + len;
return result;
}
};
parent_allocator: std.mem.Allocator,
current: ?*BufNode,
first: ?*BufNode,
pub fn init(parent: std.mem.Allocator) OptimizedArena {
return .{
.parent_allocator = parent,
.current = null,
.first = null,
};
}
pub fn deinit(self: *OptimizedArena) void {
var node = self.first;
while (node) |n| {
const next = n.next;
const total_size = @sizeOf(BufNode) + n.capacity;
const ptr = @as([*]u8, @ptrCast(n));
self.parent_allocator.free(ptr[0..total_size]);
node = next;
}
}
fn allocBuffer(self: *OptimizedArena, min_size: usize) !*BufNode {
const node_size = @sizeOf(BufNode);
const total_size = node_size + min_size;
const buffer = try self.parent_allocator.alloc(u8, total_size);
const node = @as(*BufNode, @ptrCast(@alignCast(buffer.ptr)));
node.* = .{
.next = null,
.capacity = min_size,
.used = 0,
};
// リストに追加
if (self.first == null) {
self.first = node;
} else if (self.current) |current| {
current.next = node;
}
self.current = node;
return node;
}
pub fn allocator(self: *OptimizedArena) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: u8,
ret_addr: usize,
) ?[*]u8 {
_ = ret_addr;
const self = @as(*OptimizedArena, @ptrCast(@alignCast(ctx)));
const alignment = @as(usize, 1) << @as(u6, @intCast(ptr_align));
// 現在のバッファから割り当てを試みる
if (self.current) |node| {
if (node.allocFromBuffer(len, alignment)) |buffer| {
return buffer.ptr;
}
}
// 新しいバッファが必要
const current_size = if (self.current) |n| n.capacity else 0;
const new_size = if (current_size == 0)
@max(4096, len + 256)
else
@max(current_size * 2, len + 256);
const new_node = self.allocBuffer(new_size) catch return null;
const buffer = new_node.allocFromBuffer(len, alignment) orelse return null;
return buffer.ptr;
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
_ = ctx;
_ = buf;
_ = buf_align;
_ = new_len;
_ = ret_addr;
return false;
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
_ = ctx;
_ = buf;
_ = buf_align;
_ = ret_addr;
}
};
高度なArenaパターン
パターン1: ダブルバッファリングArena
ゲームエンジンで使用される技法:
const std = @import("std");
pub const DoubleBufferedArena = struct {
arenas: [2]std.heap.ArenaAllocator,
current_index: usize,
pub fn init(base_allocator: std.mem.Allocator) DoubleBufferedArena {
return .{
.arenas = .{
std.heap.ArenaAllocator.init(base_allocator),
std.heap.ArenaAllocator.init(base_allocator),
},
.current_index = 0,
};
}
pub fn deinit(self: *DoubleBufferedArena) void {
self.arenas[0].deinit();
self.arenas[1].deinit();
}
/// 現在のフレームのアロケータを取得
pub fn currentAllocator(self: *DoubleBufferedArena) std.mem.Allocator {
return self.arenas[self.current_index].allocator();
}
/// 前フレームのアロケータを取得(読み取り専用)
pub fn previousAllocator(self: *DoubleBufferedArena) std.mem.Allocator {
const prev = (self.current_index + 1) % 2;
return self.arenas[prev].allocator();
}
/// フレームを切り替え
pub fn swap(self: *DoubleBufferedArena) void {
// 古いフレームをリセット(これから使うバッファ)
const next_index = (self.current_index + 1) % 2;
_ = self.arenas[next_index].reset(.retain_capacity);
self.current_index = next_index;
}
};
// 使用例: 非同期レンダリング
const RenderCommand = struct {
position: [3]f32,
color: [4]f32,
};
pub fn renderLoop() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var double_arena = DoubleBufferedArena.init(gpa.allocator());
defer double_arena.deinit();
for (0..60) |frame| {
const current = double_arena.currentAllocator();
const previous = double_arena.previousAllocator();
// 新しいフレームのコマンドを構築
var commands = std.ArrayList(RenderCommand).init(current);
try commands.append(.{
.position = .{ 0, 0, 0 },
.color = .{ 1, 1, 1, 1 },
});
// GPUは前フレームのデータをレンダリング中
// previousAllocatorのデータはまだ有効
std.debug.print("Frame {}: {} commands\n", .{ frame, commands.items.len });
// フレーム切り替え
double_arena.swap();
}
}
パターン2: 階層的Arena
複雑なアプリケーションでのメモリ管理:
const std = @import("std");
pub const HierarchicalArena = struct {
/// プログラム全体で保持
permanent: std.heap.ArenaAllocator,
/// レベル/シーンごと
level: std.heap.ArenaAllocator,
/// フレームごとにリセット
frame: std.heap.ArenaAllocator,
/// 関数スコープの一時データ
temp_buffer: [64 * 1024]u8,
temp: std.heap.FixedBufferAllocator,
pub fn init(base_allocator: std.mem.Allocator) HierarchicalArena {
var self: HierarchicalArena = undefined;
self.permanent = std.heap.ArenaAllocator.init(base_allocator);
self.level = std.heap.ArenaAllocator.init(base_allocator);
self.frame = std.heap.ArenaAllocator.init(base_allocator);
self.temp = std.heap.FixedBufferAllocator.init(&self.temp_buffer);
return self;
}
pub fn deinit(self: *HierarchicalArena) void {
self.frame.deinit();
self.level.deinit();
self.permanent.deinit();
}
/// レベルロード時
pub fn loadLevel(self: *HierarchicalArena) void {
_ = self.level.reset(.free_all);
}
/// フレーム開始時
pub fn newFrame(self: *HierarchicalArena) void {
_ = self.frame.reset(.retain_capacity);
self.temp.reset();
}
/// 各階層のアロケータ取得
pub fn permanentAllocator(self: *HierarchicalArena) std.mem.Allocator {
return self.permanent.allocator();
}
pub fn levelAllocator(self: *HierarchicalArena) std.mem.Allocator {
return self.level.allocator();
}
pub fn frameAllocator(self: *HierarchicalArena) std.mem.Allocator {
return self.frame.allocator();
}
pub fn tempAllocator(self: *HierarchicalArena) std.mem.Allocator {
return self.temp.allocator();
}
};
// 使用例
pub fn gameExample() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = HierarchicalArena.init(gpa.allocator());
defer arena.deinit();
// ゲーム設定(永続)
const config = try arena.permanentAllocator().create(struct {
width: u32 = 1920,
height: u32 = 1080,
});
_ = config;
// レベルロード
arena.loadLevel();
const level_data = try arena.levelAllocator().alloc(u8, 1024 * 1024);
_ = level_data;
// ゲームループ
for (0..60) |_| {
arena.newFrame();
// フレーム内の処理
const frame_data = try arena.frameAllocator().alloc(u8, 4096);
_ = frame_data;
// 一時データ
fn processBatch(alloc: std.mem.Allocator) !void {
const temp = try alloc.alloc(u8, 1024);
defer alloc.free(temp); // tempAllocatorはfree不要だが互換性のため
_ = temp;
}
try processBatch(arena.tempAllocator());
}
}
パターン3: スコープ付きArena
RAIIスタイルのArena管理:
const std = @import("std");
pub fn ScopedArena(comptime ResetMode: std.heap.ArenaAllocator.ResetMode) type {
return struct {
const Self = @This();
arena: *std.heap.ArenaAllocator,
parent_allocator: std.mem.Allocator,
pub fn init(arena: *std.heap.ArenaAllocator) Self {
return .{
.arena = arena,
.parent_allocator = arena.allocator(),
};
}
pub fn deinit(self: *Self) void {
_ = self.arena.reset(ResetMode);
}
pub fn allocator(self: *Self) std.mem.Allocator {
return self.parent_allocator;
}
};
}
// 使用例
pub fn processData(base_arena: *std.heap.ArenaAllocator) !void {
// スコープ開始
var scoped = ScopedArena(.retain_capacity).init(base_arena);
defer scoped.deinit(); // スコープ終了時に自動リセット
const allocator = scoped.allocator();
// このスコープ内でメモリを自由に使える
const data1 = try allocator.alloc(u8, 1000);
const data2 = try allocator.alloc(i32, 500);
const data3 = try allocator.alloc(f64, 200);
// 処理...
_ = data1;
_ = data2;
_ = data3;
// deferで自動的にリセットされる
}
パフォーマンス最適化
最適化1: プリアロケーション
事前にバッファを確保して再割り当てを避ける:
const std = @import("std");
pub const PreallocatedArena = struct {
arena: std.heap.ArenaAllocator,
preallocated: bool,
pub fn init(base_allocator: std.mem.Allocator) PreallocatedArena {
return .{
.arena = std.heap.ArenaAllocator.init(base_allocator),
.preallocated = false,
};
}
pub fn deinit(self: *PreallocatedArena) void {
self.arena.deinit();
}
/// 予想されるサイズを事前に確保
pub fn preallocate(self: *PreallocatedArena, size: usize) !void {
if (self.preallocated) return;
// ダミー割り当てでバッファを確保
const dummy = try self.arena.allocator().alloc(u8, size);
_ = dummy;
// リセットしてもキャパシティは保持
_ = self.arena.reset(.retain_capacity);
self.preallocated = true;
}
pub fn allocator(self: *PreallocatedArena) std.mem.Allocator {
return self.arena.allocator();
}
pub fn reset(self: *PreallocatedArena) void {
_ = self.arena.reset(.retain_capacity);
}
};
// ベンチマーク
pub fn benchmarkPreallocation() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// プリアロケーションなし
{
var arena = PreallocatedArena.init(gpa.allocator());
defer arena.deinit();
var timer = try std.time.Timer.start();
for (0..1000) |_| {
const data = try arena.allocator().alloc(u8, 1024);
_ = data;
arena.reset();
}
const elapsed = timer.read();
std.debug.print("Without prealloc: {} ns\n", .{elapsed});
}
// プリアロケーションあり
{
var arena = PreallocatedArena.init(gpa.allocator());
defer arena.deinit();
try arena.preallocate(2 * 1024 * 1024); // 2MB事前確保
var timer = try std.time.Timer.start();
for (0..1000) |_| {
const data = try arena.allocator().alloc(u8, 1024);
_ = data;
arena.reset();
}
const elapsed = timer.read();
std.debug.print("With prealloc: {} ns\n", .{elapsed});
}
}
最適化2: アライメント最適化
const std = @import("std");
pub const AlignmentOptimizedArena = struct {
arena: std.heap.ArenaAllocator,
cache_line_size: usize,
pub fn init(base_allocator: std.mem.Allocator) AlignmentOptimizedArena {
return .{
.arena = std.heap.ArenaAllocator.init(base_allocator),
.cache_line_size = 64, // 典型的なキャッシュラインサイズ
};
}
pub fn deinit(self: *AlignmentOptimizedArena) void {
self.arena.deinit();
}
/// キャッシュラインアラインされた割り当て
pub fn allocCacheAligned(
self: *AlignmentOptimizedArena,
comptime T: type,
n: usize,
) ![]T {
const allocator = self.arena.allocator();
const alignment = @max(@alignOf(T), self.cache_line_size);
return allocator.alignedAlloc(T, alignment, n);
}
/// SIMD用のアライメント(AVX2: 32バイト)
pub fn allocSIMDAligned(
self: *AlignmentOptimizedArena,
comptime T: type,
n: usize,
) ![]T {
const allocator = self.arena.allocator();
return allocator.alignedAlloc(T, 32, n);
}
};
最適化3: メモリプールとの組み合わせ
const std = @import("std");
pub const HybridArena = struct {
/// 小さい割り当て用(< 256バイト)
small_pool: std.heap.MemoryPool(SmallBlock),
/// 中程度の割り当て用(256バイト - 4KB)
medium_arena: std.heap.ArenaAllocator,
/// 大きい割り当て用(> 4KB)
large_allocator: std.mem.Allocator,
const SmallBlock = struct {
data: [256]u8,
};
pub fn init(base_allocator: std.mem.Allocator) HybridArena {
return .{
.small_pool = std.heap.MemoryPool(SmallBlock).init(base_allocator),
.medium_arena = std.heap.ArenaAllocator.init(base_allocator),
.large_allocator = base_allocator,
};
}
pub fn deinit(self: *HybridArena) void {
self.small_pool.deinit();
self.medium_arena.deinit();
}
pub fn allocator(self: *HybridArena) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(
ctx: *anyopaque,
len: usize,
ptr_align: u8,
ret_addr: usize,
) ?[*]u8 {
const self = @as(*HybridArena, @ptrCast(@alignCast(ctx)));
if (len <= 256) {
// 小さい割り当て: プールから
const block = self.small_pool.create() catch return null;
return @ptrCast(&block.data);
} else if (len <= 4096) {
// 中程度: Arenaから
return self.medium_arena.allocator().vtable.alloc(
self.medium_arena.allocator().ptr,
len,
ptr_align,
ret_addr,
);
} else {
// 大きい: 直接割り当て
return self.large_allocator.vtable.alloc(
self.large_allocator.ptr,
len,
ptr_align,
ret_addr,
);
}
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
_ = ctx;
_ = buf;
_ = buf_align;
_ = new_len;
_ = ret_addr;
return false;
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
_ = ctx;
_ = buf;
_ = buf_align;
_ = ret_addr;
}
};
実世界のケーススタディ
ケース1: Zigコンパイラ
Zigコンパイラのメモリ管理戦略:
Phase 1: Tokenization
Arena: Token Arena
ライフタイム: パース完了まで
Phase 2: Parsing
Arena: AST Arena
ライフタイム: セマンティック解析完了まで
Phase 3: Semantic Analysis
Arena: Type Arena (永続)
Arena: 関数ごとのローカルArena
ライフタイム: コンパイル全体 / 関数解析中
Phase 4: Code Generation
Arena: IR Arena
ライフタイム: 最適化パスごと
メモリ使用量:
小規模プロジェクト: ~50MB
大規模プロジェクト: ~500MB
ピーク時: AST + 型情報
ケース2: ゲームエンジン(Unity相当)
階層的メモリ管理:
1. Global Heap (GPA)
├─ Permanent Arena
│ └─ エンジン設定、グローバルリソース
│
├─ Scene Arena
│ └─ シーンオブジェクト、スクリプト
│
├─ Frame Arena (Double Buffered)
│ ├─ Current Frame
│ │ └─ レンダリングコマンド、パーティクル
│ └─ Previous Frame (GPU読み取り中)
│
└─ Temp Stack (64KB固定)
└─ 関数ローカルの一時データ
メモリ使用量(典型的なシーン):
Permanent: 100MB
Scene: 500MB
Frame: 10MB/frame
Temp: 64KB(固定)
ケース3: Webサーバー(Hono相当)
const std = @import("std");
const WebServer = struct {
const Request = struct {
method: []const u8,
path: []const u8,
headers: std.StringHashMap([]const u8),
body: []const u8,
};
const Response = struct {
status: u16,
headers: std.StringHashMap([]const u8),
body: []const u8,
};
fn handleRequest(
base_allocator: std.mem.Allocator,
raw_request: []const u8,
) !Response {
// リクエストスコープArena
var arena = std.heap.ArenaAllocator.init(base_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// パース(メモリを自由に使える)
const request = try parseRequest(allocator, raw_request);
// ルーティング
if (std.mem.eql(u8, request.path, "/api/users")) {
return try handleUsers(allocator, request);
}
// デフォルトレスポンス
return Response{
.status = 404,
.headers = std.StringHashMap([]const u8).init(allocator),
.body = try allocator.dupe(u8, "Not Found"),
};
}
fn parseRequest(
allocator: std.mem.Allocator,
raw: []const u8,
) !Request {
return Request{
.method = try allocator.dupe(u8, "GET"),
.path = try allocator.dupe(u8, "/api/users"),
.headers = std.StringHashMap([]const u8).init(allocator),
.body = try allocator.dupe(u8, raw),
};
}
fn handleUsers(
allocator: std.mem.Allocator,
request: Request,
) !Response {
_ = request;
var response = Response{
.status = 200,
.headers = std.StringHashMap([]const u8).init(allocator),
.body = undefined,
};
try response.headers.put(
try allocator.dupe(u8, "Content-Type"),
try allocator.dupe(u8, "application/json"),
);
response.body = try allocator.dupe(u8, "{\"users\": []}");
return response;
}
};
まとめ
Arenaアロケータの深層を学びました:
- 設計哲学: ライフタイムのグループ化
- 内部実装: SinglyLinkedList、成長戦略
- 高度なパターン: ダブルバッファリング、階層的管理
- 最適化技法: プリアロケーション、アライメント
- 実世界の応用: コンパイラ、ゲーム、Webサーバー
- Zig Standard Library - ArenaAllocator: https://github.com/ziglang/zig/blob/master/lib/std/heap/arena_allocator.zig
- Ginger Bill - Memory Allocation Strategies: https://www.gingerbill.org/series/memory-allocation-strategies/
- Niklaus Wirth - Algorithms + Data Structures = Programs (1976)
次の章では、Poolアロケータについて学びます。固定サイズのオブジェクトを効率的に管理する技法を探ります。