第12章: メモリ管理
学習目標
この章を終えると、以下ができるようになります:
- Zigのアロケータ抽象化を理解できる
- 標準アロケータを適切に選択して使用できる
- カスタムアロケータを実装できる
- メモリリークを検出し、防ぐことができる
アロケータの基本
アロケータインターフェース
Zigでは、すべてのアロケータが統一されたインターフェースを実装します。
const std = @import("std");
pub fn example() !void {
// ページアロケータ(シンプルだが遅い)
const page_allocator = std.heap.page_allocator;
// メモリを確保
const bytes = try page_allocator.alloc(u8, 100);
defer page_allocator.free(bytes);
// 使用
for (bytes, 0..) |*byte, i| {
byte.* = @intCast(i % 256);
}
std.debug.print("Allocated {} bytes\n", .{bytes.len});
std.debug.print("First 10 bytes: ", .{});
for (bytes[0..10]) |byte| {
std.debug.print("{} ", .{byte});
}
std.debug.print("\n", .{});
}
基本的なメモリ操作
const std = @import("std");
pub fn example() !void {
const allocator = std.heap.page_allocator;
// alloc: スライスを確保
const numbers = try allocator.alloc(i32, 10);
defer allocator.free(numbers);
// create: 単一の値を確保
const value = try allocator.create(i32);
defer allocator.destroy(value);
value.* = 42;
// 初期化
for (numbers, 0..) |*num, i| {
num.* = @intCast(i * 10);
}
std.debug.print("Value: {}\n", .{value.*});
std.debug.print("Numbers: ", .{});
for (numbers) |num| {
std.debug.print("{} ", .{num});
}
std.debug.print("\n", .{});
}
realloc: メモリのリサイズ
const std = @import("std");
pub fn example() !void {
const allocator = std.heap.page_allocator;
// 初期確保
var buffer = try allocator.alloc(u8, 10);
defer allocator.free(buffer);
std.debug.print("Initial size: {}\n", .{buffer.len});
// リサイズ(拡張)
buffer = try allocator.realloc(buffer, 20);
std.debug.print("After realloc: {}\n", .{buffer.len});
// リサイズ(縮小)
buffer = try allocator.realloc(buffer, 5);
std.debug.print("After shrink: {}\n", .{buffer.len});
}
標準アロケータ
GeneralPurposeAllocator
本番環境で使用する汎用アロケータです。メモリリークを検出できます。
const std = @import("std");
pub fn example() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("Memory leak detected!\n", .{});
}
}
const allocator = gpa.allocator();
// メモリ確保
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
std.debug.print("Allocated {} bytes\n", .{data.len});
// わざとリークさせる(テスト用)
const leaked_data = try allocator.alloc(u8, 100);
_ = leaked_data; // freeを忘れる
}
ArenaAllocator
アリーナアロケータは、すべての確保を一度に解放できる便利なアロケータです。
const std = @import("std");
pub fn example() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // すべてを一度に解放
const allocator = arena.allocator();
// 複数回確保
const data1 = try allocator.alloc(u8, 100);
const data2 = try allocator.alloc(u8, 200);
const data3 = try allocator.alloc(u8, 300);
// 個別にfreeする必要なし
std.debug.print("Allocated {} + {} + {} = {} bytes\n",
.{data1.len, data2.len, data3.len, data1.len + data2.len + data3.len});
}
FixedBufferAllocator
固定サイズのバッファからメモリを確保するアロケータです。
const std = @import("std");
pub fn example() !void {
// スタック上にバッファを用意
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// バッファから確保
const data1 = try allocator.alloc(u8, 100);
const data2 = try allocator.alloc(u8, 200);
std.debug.print("Allocated {} + {} = {} bytes from stack\n",
.{data1.len, data2.len, data1.len + data2.len});
std.debug.print("Buffer used: {} bytes\n", .{fba.end_index});
// freeは必須ではない(スタック上なので)
}
StackFallbackAllocator
スタックバッファを優先的に使い、不足したらヒープを使うアロケータです。
const std = @import("std");
pub fn example() !void {
// スタック上に256バイト確保
var buffer: [256]u8 = undefined;
var stack_fallback = std.heap.stackFallback(256, std.heap.page_allocator);
const allocator = stack_fallback.get();
// 小さい確保(スタックから)
const small = try allocator.alloc(u8, 100);
defer allocator.free(small);
std.debug.print("Small allocation: {} bytes (from stack)\n", .{small.len});
// 大きい確保(ヒープから)
const large = try allocator.alloc(u8, 1024);
defer allocator.free(large);
std.debug.print("Large allocation: {} bytes (from heap)\n", .{large.len});
std.debug.print("Stack used: {}\n", .{stack_fallback.fixed_buffer_allocator.end_index});
}
カスタムアロケータの実装
統計情報を記録するアロケータ
const std = @import("std");
const StatsAllocator = struct {
parent_allocator: std.mem.Allocator,
allocations: usize,
deallocations: usize,
bytes_allocated: usize,
const Self = @This();
pub fn init(parent: std.mem.Allocator) Self {
return Self{
.parent_allocator = parent,
.allocations = 0,
.deallocations = 0,
.bytes_allocated = 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.allocations += 1;
self.bytes_allocated += len;
}
return result;
}
fn resize(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
new_len: usize,
ret_addr: usize,
) bool {
const self: *Self = @ptrCast(@alignCast(ctx));
return self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr);
}
fn free(
ctx: *anyopaque,
buf: []u8,
buf_align: u8,
ret_addr: usize,
) void {
const self: *Self = @ptrCast(@alignCast(ctx));
self.deallocations += 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("Allocations: {}\n", .{self.allocations});
std.debug.print("Deallocations: {}\n", .{self.deallocations});
std.debug.print("Bytes allocated: {}\n", .{self.bytes_allocated});
std.debug.print("Leaks: {}\n", .{self.allocations - self.deallocations});
}
};
pub fn example() !void {
var stats = StatsAllocator.init(std.heap.page_allocator);
const allocator = stats.allocator();
const data1 = try allocator.alloc(u8, 100);
defer allocator.free(data1);
const data2 = try allocator.alloc(u8, 200);
defer allocator.free(data2);
stats.printStats();
}
プールアロケータ
const std = @import("std");
fn PoolAllocator(comptime T: type, comptime pool_size: usize) type {
return struct {
pool: [pool_size]T,
free_list: [pool_size]bool,
next_free: usize,
const Self = @This();
pub fn init() Self {
return Self{
.pool = undefined,
.free_list = [_]bool{true} ** pool_size,
.next_free = 0,
};
}
pub fn alloc(self: *Self) ?*T {
// 空きを探す
var i = self.next_free;
var count: usize = 0;
while (count < pool_size) : (count += 1) {
if (self.free_list[i]) {
self.free_list[i] = false;
self.next_free = (i + 1) % pool_size;
return &self.pool[i];
}
i = (i + 1) % pool_size;
}
return null;
}
pub fn free(self: *Self, ptr: *T) void {
const index = (@intFromPtr(ptr) - @intFromPtr(&self.pool[0])) / @sizeOf(T);
if (index < pool_size) {
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 = pool_size,
.used = used,
.free = pool_size - used,
};
}
};
}
pub fn example() !void {
const Node = struct {
value: i32,
next: ?*@This(),
};
var pool = PoolAllocator(Node, 10).init();
// 確保
const node1 = pool.alloc() orelse return error.OutOfMemory;
const node2 = pool.alloc() orelse return error.OutOfMemory;
const node3 = pool.alloc() orelse return error.OutOfMemory;
node1.* = .{ .value = 1, .next = node2 };
node2.* = .{ .value = 2, .next = node3 };
node3.* = .{ .value = 3, .next = null };
const s = pool.stats();
std.debug.print("Pool stats: {}/{} used, {} free\n", .{s.used, s.total, s.free});
// 解放
pool.free(node2);
const s2 = pool.stats();
std.debug.print("After free: {}/{} used, {} free\n", .{s2.used, s2.total, s2.free});
}
メモリリークの検出
メモリリークのデバッグ
const std = @import("std");
pub fn example() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
// デバッグオプションを有効化
.safety = true,
.thread_safe = true,
}){};
defer {
const leaked = gpa.deinit();
if (leaked == .leak) {
std.debug.print("ERROR: Memory leak detected!\n", .{});
} else {
std.debug.print("No memory leaks\n", .{});
}
}
const allocator = gpa.allocator();
// 正しい使用
{
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
}
// リークする使用(テスト用)
{
const leaked = try allocator.alloc(u8, 50);
_ = leaked;
// freeを忘れている!
}
}
デバッグアロケータ
const std = @import("std");
pub fn example() !void {
// デバッグビルドでメモリ破壊を検出
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var data = try allocator.alloc(u8, 10);
defer allocator.free(data);
// 境界を越えた書き込み(デバッグビルドで検出される)
// data[10] = 42; // パニック!
}
実践的なパターン
アリーナを使った一時メモリ
const std = @import("std");
fn processData(arena_allocator: std.mem.Allocator) !void {
// すべての確保はarenaから行う
const temp_buffer = try arena_allocator.alloc(u8, 1024);
const temp_list = try arena_allocator.alloc(i32, 100);
// 処理
for (temp_list, 0..) |*item, i| {
item.* = @intCast(i * 2);
}
std.debug.print("Processed {} items\n", .{temp_list.len});
// 個別にfreeしない!arenaが一括で解放
}
pub fn example() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
try processData(allocator);
try processData(allocator);
try processData(allocator);
// すべての確保が一度に解放される
}
アロケータの切り替え
const std = @import("std");
const DataProcessor = struct {
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) DataProcessor {
return .{ .allocator = allocator };
}
pub fn process(self: DataProcessor, size: usize) !void {
const buffer = try self.allocator.alloc(u8, size);
defer self.allocator.free(buffer);
std.debug.print("Processing {} bytes\n", .{buffer.len});
}
};
pub fn example() !void {
// テスト環境: FixedBufferAllocator
{
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const processor = DataProcessor.init(allocator);
try processor.process(512);
}
// 本番環境: GeneralPurposeAllocator
{
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const processor = DataProcessor.init(allocator);
try processor.process(512);
}
}
メモリプール
const std = @import("std");
const Buffer = struct {
data: []u8,
in_use: bool,
};
const BufferPool = struct {
buffers: []Buffer,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, count: usize, size: usize) !BufferPool {
const buffers = try allocator.alloc(Buffer, count);
for (buffers) |*buffer| {
buffer.* = .{
.data = try allocator.alloc(u8, size),
.in_use = false,
};
}
return BufferPool{
.buffers = buffers,
.allocator = allocator,
};
}
pub fn deinit(self: *BufferPool) void {
for (self.buffers) |buffer| {
self.allocator.free(buffer.data);
}
self.allocator.free(self.buffers);
}
pub fn acquire(self: *BufferPool) ?[]u8 {
for (self.buffers) |*buffer| {
if (!buffer.in_use) {
buffer.in_use = true;
return buffer.data;
}
}
return null;
}
pub fn release(self: *BufferPool, data: []u8) void {
for (self.buffers) |*buffer| {
if (buffer.data.ptr == data.ptr) {
buffer.in_use = false;
return;
}
}
}
};
pub fn example() !void {
var pool = try BufferPool.init(std.heap.page_allocator, 3, 1024);
defer pool.deinit();
// バッファを取得
const buf1 = pool.acquire() orelse return error.NoBufferAvailable;
const buf2 = pool.acquire() orelse return error.NoBufferAvailable;
std.debug.print("Acquired 2 buffers\n", .{});
// バッファを返却
pool.release(buf1);
std.debug.print("Released 1 buffer\n", .{});
// 再利用
const buf3 = pool.acquire() orelse return error.NoBufferAvailable;
_ = buf3;
std.debug.print("Re-acquired buffer\n", .{});
}
まとめ
この章では、Zigのメモリ管理について学びました:
次の章では、テストについて学びます。