第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のメモリ管理について学びました:

  • アロケータ抽象化: 統一されたインターフェース
  • 標準アロケータ: GPA、Arena、FixedBuffer、StackFallback
  • カスタムアロケータ: 統計情報、プール
  • メモリリーク検出: GPAのデバッグ機能
  • 実践的パターン: アリーナ、プール、アロケータ切り替え
  • 次の章では、テストについて学びます。

    参考資料

  • Zig Language Reference - Memory: https://ziglang.org/documentation/master/#Memory
  • Zig Standard Library - Heap: https://ziglang.org/documentation/master/std/#std.heap
  • Ziglearn - Memory Management: https://ziglearn.org/chapter-2/#allocators