解答12: メモリ管理の実践
概要
この解答では、Zigのメモリ管理システムを深く理解し、カスタムアロケータの実装、メモリプール、アリーナアロケータの活用、メモリリーク検出について学びます。
解答例
Part 1: カスタムアロケータ実装
const std = @import("std");
const TrackingAllocator = struct {
parent_allocator: std.mem.Allocator,
total_allocated: usize,
total_freed: usize,
current_allocated: usize,
peak_allocated: usize,
allocation_count: usize,
free_count: usize,
const Self = @This();
pub fn init(parent: std.mem.Allocator) Self {
return Self{
.parent_allocator = parent,
.total_allocated = 0,
.total_freed = 0,
.current_allocated = 0,
.peak_allocated = 0,
.allocation_count = 0,
.free_count = 0,
};
}
pub fn allocator(self: *Self) 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: *Self = @ptrCast(@alignCast(ctx));
const result = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr);
if (result != null) {
self.total_allocated += len;
self.current_allocated += len;
self.allocation_count += 1;
if (self.current_allocated > self.peak_allocated) {
self.peak_allocated = self.current_allocated;
}
}
return result;
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
const self: *Self = @ptrCast(@alignCast(ctx));
const old_len = buf.len;
const success = self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr);
if (success) {
if (new_len > old_len) {
const delta = new_len - old_len;
self.total_allocated += delta;
self.current_allocated += delta;
if (self.current_allocated > self.peak_allocated) {
self.peak_allocated = self.current_allocated;
}
} else {
const delta = old_len - new_len;
self.total_freed += delta;
self.current_allocated -= delta;
}
}
return success;
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
const self: *Self = @ptrCast(@alignCast(ctx));
self.total_freed += buf.len;
self.current_allocated -= buf.len;
self.free_count += 1;
self.parent_allocator.rawFree(buf, buf_align, ret_addr);
}
pub fn printStats(self: Self) void {
std.debug.print("=== Allocator Statistics ===\n", .{});
std.debug.print("Total allocated: {} bytes\n", .{self.total_allocated});
std.debug.print("Total freed: {} bytes\n", .{self.total_freed});
std.debug.print("Currently allocated: {} bytes\n", .{self.current_allocated});
std.debug.print("Peak allocation: {} bytes\n", .{self.peak_allocated});
std.debug.print("Allocation count: {}\n", .{self.allocation_count});
std.debug.print("Free count: {}\n", .{self.free_count});
}
};
pub fn main() !void {
var tracking = TrackingAllocator.init(std.heap.page_allocator);
const allocator = tracking.allocator();
const data1 = try allocator.alloc(u8, 1024);
const data2 = try allocator.alloc(u8, 2048);
const data3 = try allocator.alloc(u8, 512);
allocator.free(data1);
allocator.free(data2);
const data4 = try allocator.alloc(u8, 4096);
allocator.free(data3);
allocator.free(data4);
tracking.printStats();
}
Part 2: メモリプール実装
const std = @import("std");
fn MemoryPool(comptime T: type, comptime capacity: usize) type {
return struct {
pool: [capacity]T,
free_list: [capacity]bool,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.pool = undefined,
.free_list = [_]bool{true} ** capacity,
.allocator = allocator,
};
}
pub fn alloc(self: *Self) ?*T {
for (self.free_list, 0..) |is_free, i| {
if (is_free) {
self.free_list[i] = false;
return &self.pool[i];
}
}
return null;
}
pub fn free(self: *Self, ptr: *T) void {
const pool_start = @intFromPtr(&self.pool[0]);
const pool_end = @intFromPtr(&self.pool[capacity - 1]);
const ptr_addr = @intFromPtr(ptr);
if (ptr_addr >= pool_start and ptr_addr <= pool_end) {
const index = (ptr_addr - pool_start) / @sizeOf(T);
if (index < capacity) {
self.free_list[index] = true;
}
}
}
pub fn stats(self: Self) struct {
total: usize,
used: usize,
free: usize,
} {
var used: usize = 0;
for (self.free_list) |is_free| {
if (!is_free) used += 1;
}
return .{
.total = capacity,
.used = used,
.free = capacity - used,
};
}
pub fn reset(self: *Self) void {
self.free_list = [_]bool{true} ** capacity;
}
};
}
const Node = struct {
value: i32,
next: ?*Node,
};
pub fn main() !void {
var pool = MemoryPool(Node, 10).init(std.heap.page_allocator);
std.debug.print("=== Memory Pool ===\n\n", .{});
// ノードを確保してリンクリストを作成
const n1 = pool.alloc() orelse return error.OutOfMemory;
const n2 = pool.alloc() orelse return error.OutOfMemory;
const n3 = pool.alloc() orelse return error.OutOfMemory;
n1.* = .{ .value = 1, .next = n2 };
n2.* = .{ .value = 2, .next = n3 };
n3.* = .{ .value = 3, .next = null };
var s = pool.stats();
std.debug.print("After allocation: {}/{} used\n", .{s.used, s.total});
// リンクリストを辿る
var current: ?*Node = n1;
std.debug.print("List: ", .{});
while (current) |node| {
std.debug.print("{} -> ", .{node.value});
current = node.next;
}
std.debug.print("null\n\n", .{});
// 一部を解放
pool.free(n2);
s = pool.stats();
std.debug.print("After free: {}/{} used\n", .{s.used, s.total});
// 再確保
const n4 = pool.alloc() orelse return error.OutOfMemory;
n4.* = .{ .value = 4, .next = null };
s = pool.stats();
std.debug.print("After realloc: {}/{} used\n", .{s.used, s.total});
}
Part 3: アリーナアロケータの活用
const std = @import("std");
const JsonValue = union(enum) {
Null,
Bool: bool,
Number: f64,
String: []const u8,
Array: []JsonValue,
Object: std.StringHashMap(JsonValue),
};
// Arenaを使ってJSONをパース(簡易版)
fn parseJson(arena: std.mem.Allocator, json_str: []const u8) !JsonValue {
_ = json_str;
// 実装(簡略化のため固定値を返す)
const array = try arena.alloc(JsonValue, 3);
array[0] = JsonValue{ .Number = 1.0 };
array[1] = JsonValue{ .Number = 2.0 };
array[2] = JsonValue{ .Number = 3.0 };
return JsonValue{ .Array = array };
}
// Arenaを使って文字列を連結
fn concatStrings(arena: std.mem.Allocator, strings: []const []const u8) ![]const u8 {
var total_len: usize = 0;
for (strings) |str| {
total_len += str.len;
}
const result = try arena.alloc(u8, total_len);
var offset: usize = 0;
for (strings) |str| {
@memcpy(result[offset..][0..str.len], str);
offset += str.len;
}
return result;
}
// Arenaを使って動的配列を構築
fn buildDynamicArray(arena: std.mem.Allocator, count: usize) ![]i32 {
const result = try arena.alloc(i32, count);
for (result, 0..) |*item, i| {
item.* = @intCast(i * i);
}
return result;
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
std.debug.print("=== Arena Allocator Usage ===\n\n", .{});
// JSON パース
const json = try parseJson(allocator, "[1, 2, 3]");
switch (json) {
.Array => |arr| {
std.debug.print("Parsed array with {} elements\n\n", .{arr.len});
},
else => {},
}
// 文字列連結
const parts = [_][]const u8{ "Hello", ", ", "World", "!" };
const concatenated = try concatStrings(allocator, &parts);
std.debug.print("Concatenated: {s}\n\n", .{concatenated});
// 動的配列
const array = try buildDynamicArray(allocator, 10);
std.debug.print("Dynamic array: ", .{});
for (array) |item| {
std.debug.print("{} ", .{item});
}
std.debug.print("\n", .{});
// すべてのメモリが一度に解放される
}
Part 4: メモリリーク検出
const std = @import("std");
const LeakyContainer = struct {
data: []u8,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, size: usize) !LeakyContainer {
const data = try allocator.alloc(u8, size);
return LeakyContainer{
.data = data,
.allocator = allocator,
};
}
pub fn deinit(self: *LeakyContainer) void {
self.allocator.free(self.data);
}
pub fn clone(self: LeakyContainer) !LeakyContainer {
const new_data = try self.allocator.alloc(u8, self.data.len);
@memcpy(new_data, self.data);
return LeakyContainer{
.data = new_data,
.allocator = self.allocator,
};
}
};
// リークのあるバージョン
fn processDataLeaky(allocator: std.mem.Allocator) !void {
const data = try allocator.alloc(u8, 1024);
// freeを忘れている!
_ = data;
}
// 修正版
fn processDataFixed(allocator: std.mem.Allocator) !void {
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
// 処理
for (data, 0..) |*byte, i| {
byte.* = @intCast(i % 256);
}
}
pub fn main() !void {
std.debug.print("=== Memory Leak Detection ===\n\n", .{});
// テスト1: リークあり
{
std.debug.print("Test 1: Leaky version\n", .{});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
try processDataLeaky(allocator);
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("Result: LEAK DETECTED\n\n", .{});
}
}
// テスト2: 修正版
{
std.debug.print("Test 2: Fixed version\n", .{});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
try processDataFixed(allocator);
const leaked = gpa.deinit();
if (leaked == .ok) {
std.debug.print("Result: NO LEAKS\n\n", .{});
}
}
// テスト3: Container
{
std.debug.print("Test 3: Container\n", .{});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var container = try LeakyContainer.init(allocator, 512);
defer container.deinit();
const leaked = gpa.deinit();
if (leaked == .ok) {
std.debug.print("Result: NO LEAKS\n", .{});
}
}
}
ポイント解説
1. アロケータVTable
Zigのアロケータインターフェースは、3つの関数ポインタで構成されています:
alloc: メモリを確保resize: メモリをリサイズfree: メモリを解放- 一時的なデータ構造
- リクエスト/レスポンス処理
- パーサーや コンパイラの中間データ
- ゲームエンジンの GameObject
- ネットワークパケット
- パーサーのノード
- メモリリーク検出
- 境界チェック
- スレッドセーフ
2. アリーナアロケータの利点
アリーナアロケータは、以下のケースで特に有効です:
3. メモリプールの使い所
固定サイズのオブジェクトを頻繁に確保/解放する場合に効果的です:
4. GeneralPurposeAllocator
本番環境で推奨されるアロケータで、以下の機能があります:
よくある間違い
1. deferの忘れ
// 間違い
const data = try allocator.alloc(u8, 100);
// 処理...
// freeを忘れている!
// 正しい
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
2. アリーナの誤用
// 間違い
var arena = std.heap.ArenaAllocator.init(allocator);
const data = try arena.allocator().alloc(u8, 100);
arena.allocator().free(data); // 不要!
arena.deinit();
// 正しい
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); // これだけでOK
const data = try arena.allocator().alloc(u8, 100);
3. resize の戻り値無視
// 間違い
_ = allocator.resize(buffer, new_size); // 失敗するかもしれない
// 正しい
if (!allocator.resize(buffer, new_size)) {
buffer = try allocator.realloc(buffer, new_size);
}
発展課題
Challenge 1: スラブアロケータ
異なるサイズクラス(32B, 64B, 128B...)を持つスラブアロケータを実装してください。
Challenge 2: コピーオンライト
参照カウントとコピーオンライトを組み合わせた文字列型を実装してください。
Challenge 3: メモリプロファイラー
アロケーション情報(サイズ、場所、スタックトレース)を記録するプロファイラーを作成してください。
Challenge 4: カスタムGC
マーク&スイープまたは参照カウント方式の簡易ガベージコレクターを実装してください。
Challenge 5: メモリ圧縮
フラグメンテーションを減らすために、メモリを圧縮するアロケータを実装してください。
まとめ
この課題を通じて、以下を学びました:
Zigのメモリ管理システムは、柔軟性とパフォーマンスを両立させた優れた設計です。適切なアロケータを選択することで、メモリ効率を最大化できます。