解答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: メモリを解放
  • 2. アリーナアロケータの利点

    アリーナアロケータは、以下のケースで特に有効です:

  • 一時的なデータ構造
  • リクエスト/レスポンス処理
  • パーサーや コンパイラの中間データ
  • 3. メモリプールの使い所

    固定サイズのオブジェクトを頻繁に確保/解放する場合に効果的です:

  • ゲームエンジンの GameObject
  • ネットワークパケット
  • パーサーのノード
  • 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: メモリ圧縮

    フラグメンテーションを減らすために、メモリを圧縮するアロケータを実装してください。

    まとめ

    この課題を通じて、以下を学びました:

  • カスタムアロケータ: VTableを実装してカスタムアロケータを作成
  • メモリプール: 固定サイズオブジェクトの効率的な管理
  • アリーナアロケータ: 一時データの簡潔な管理
  • リーク検出: GeneralPurposeAllocatorによるデバッグ

Zigのメモリ管理システムは、柔軟性とパフォーマンスを両立させた優れた設計です。適切なアロケータを選択することで、メモリ効率を最大化できます。