ざっくりZig - アロケータの使い方

2024/06/18に公開

アロケータ(Allocator)は処理の実行中に新たなメモリ領域を確保するときと確保した領域を解放するときに使用します。var buf: [3]u8 = undefined;のようにあらかじめ確保されたバッファ領域はアロケータの対象になりません。

まずはテスト用のアロケータであるstd.testing.allocatorを直接使ってアロケータの基本的な操作をしてみます。このアロケータはtest {...}の部分でしか使用できません。

単独の値を格納するメモリ領域の確保と解放

u8型をはじめ、単独の値を格納するメモリ領域の確保はcreate(型名)で行います。createの戻り値はポインタで、メモリを確保できないときはerror.OutOfMemoryが返されます。そして確保したメモリ領域の解放はdestroy(変数名)で行います。引数はcreateで返されたポインタを持つ変数です。destroycreateのすぐ後にdeferで最後に実行されるようにしておくと解放を忘れにくいでしょう。

単独の値を格納するメモリ領域の確保と解放
// zig test ソースファイル名 で実行

const std = @import("std");

test "testing.allocator - value" {
    const print = std.debug.print;

    const allocator = std.testing.allocator;

    // アロケータによるメモリ領域の確保(単独)
    const buf = try allocator.create(u8);
    // 最後にアロケータによるメモリ領域の解放(単独)
    defer allocator.destroy(buf);

    buf.* = 1;
    print("@TypeOf(buf) = {}, buf.* = {}\n", .{@TypeOf(buf), buf.*});
}
結果
@TypeOf(buf) = *u8, buf.* = 1

配列のためのメモリ領域の確保と解放

[3]u8のようなまとまったメモリ領域を確保するときはalloc(型名, 要素数)を実行します。戻り値はスライスです。メモリ領域を確保できないときはerror.OutOfMemoryが返されます。確保したメモリ領域を解放するときはfree(変数名)を実行します。引数はallocで確保したスライスです。

配列のためのメモリ領域の確保と解放
test "testing.allocator - array" {
    const print = std.debug.print;
    const allocator = std.testing.allocator;

    // アロケータによるメモリ領域の確保(配列)
    const buf = try allocator.alloc(u8, 3);
    // 最後にアロケータによるメモリ領域の解放(配列)
    defer allocator.free(buf);

    buf[0] = 1;
    buf[1] = 2;
    buf[2] = 3;

    print("@TypeOf(buf) = {}, buf = {any}\n", .{@TypeOf(buf), buf});
}
結果
@TypeOf(buf) = []u8, buf = { 1, 2, 3 }

配列の長さを増やすときはrealloc(型名, 要素数)で行います。戻り値はallocと同じです。

配列のためのメモリ領域の確保と解放
test "testing.allocator - array" {
    const print = std.debug.print;
    const allocator = std.testing.allocator;

    // アロケータによるメモリ領域の確保(配列)
    var buf = try allocator.alloc(u8, 3);
    // 最後にアロケータによるメモリ領域の解放(配列)
    defer allocator.free(buf);

    buf[0] = 1;
    buf[1] = 2;
    buf[2] = 3;

    print("@TypeOf(buf) = {}, buf = {any}\n", .{@TypeOf(buf), buf});

    // メモリ領域を増やす(配列)
    buf = try allocator.realloc(buf, 5);
    buf[3] = 4;
    buf[4] = 5;
    print("@TypeOf(buf) = {}, buf = {any}\n", .{@TypeOf(buf), buf});
}
結果
@TypeOf(buf) = []u8, buf = { 1, 2, 3 }
@TypeOf(buf) = []u8, buf = { 1, 2, 3, 4, 5 }

文字列の領域

文字列の領域は番兵(Sentinel)付きの配列が必要になる場合があります。その場合はallocSentinel(u8, 要素数, 0)を実行してメモリ領域を確保します。要素数は文字数ではなく文字列の格納に必要なバイト数であることに注意してください。日本語(UTF-8のみ対応[1])の場合、半角の英数字と記号以外は1文字につき3バイトです。

文字列領域の確保と解放
test "testing.allocator - string" {
    const print = std.debug.print;
    const allocator = std.testing.allocator;

    // 文字列領域の確保と解放
    const buf = try allocator.allocSentinel(u8, 19, 0);
    defer allocator.free(buf);

    @memcpy(buf, "こんにちはZig!");
    print("@TypeOf(buf) = {}, buf = {s}\n", .{@TypeOf(buf), buf});
    print("buf.len = {}, buf.ptr[0..15] = {s}, buf.ptr[15..19] = {s}\n", .{buf.len, buf.ptr[0..15], buf.ptr[15..19]});
}
結果
@TypeOf(buf) = [:0]u8, buf = こんにちはZig!
buf.len = 19, buf.ptr[0..15] = こんにちは, buf.ptr[15..19] = Zig!

確保と解放のエラー

std.testing.allocatorが返すメモリ領域の確保と解放を行うときに起こる可能性のあるエラーをいくつか紹介します。

解放忘れ

メモリの解放が行われなかった領域があるとエラーとして知らせてくれます。

メモリ領域の解放忘れ
const allocator = std.testing.allocator;

const buf = try allocator.create(u8);
_ = buf;
エラー
[gpa] (err): memory address 0x7f13694f3000 leaked: 
/path/to/filename.zig:xx:xx: 0x103e891 in test.testing.allocator - errors (test)
 const buf = try allocator.create(u8);
                                 ^
..... (snip) .....

解放の重複

同じメモリ領域の解放が重複したときはpanicとなります。

メモリ領域の重複解放
const buf = try allocator.create(u8);
defer allocator.destroy(buf);

allocator.destroy(buf);
エラー
/path/to/zig/lib/std/heap/general_purpose_allocator.zig:64
                    @panic("Invalid free");
                    ^
..... (snip) .....

確保の重複

確保済みのメモリ領域を解放せずに再び確保した場合もエラーです。

メモリ領域の重複確保
const allocator = std.testing.allocator;

var buf = try allocator.create(u8);
defer allocator.destroy(buf);

// 解放前に確保しようとしている
buf = try allocator.create(u8);
エラー
/path/to/filename.zig:xx:xx: 0x103e891 in test.testing.allocator - errors (test)
 var buf = try allocator.create(u8);
                               ^
..... (snip) .....

確保と解放の重複

確保と解放の両方が重複するとまた別のエラーになります。

メモリ領域確保と解放の重複
const allocator = std.testing.allocator;

var buf = try allocator.create(u8);
defer allocator.destroy(buf);

// メモリ領域確保と解放の重複
buf = try allocator.create(u8);
allocator.destroy(buf);
エラー
[gpa] (err): Double free detected. Allocation: 
/path/to/filename.zig:xx:xx: 0x103e8d2 in test.testing.allocator - errors (test)
 buf = try allocator.create(u8);
                           ^

..... (snip) .....

 First free: 
/path/to/filename.zig:xx:xx: 0x103e922 in test.testing.allocator - errors (test)
 allocator.destroy(buf);
                  ^
..... (snip) .....

 Second free: 
/path/to/zig/filename.zig:xx:xx: 0x103e935 in test.testing.allocator - errors (test)
 defer allocator.destroy(buf);
                        ^
..... (snip) .....

[gpa] (err): memory address 0x7fad3fb3c000 leaked: 
/path/to/filename.zig:xx:xx: 0x103e891 in test.testing.allocator - errors (test)
 var buf = try allocator.create(u8);
                               ^

代表的なアロケータ

  • std.testing.allocator
    test {...}内でのみ使用可能なアロケータでメモリ領域の確保や解放をチェックし、問題があればエラーとして知らせる

  • std.heap.page_allocator
    シンプルなアロケータでチェックはなし

std.heap.page_allocatorによるメモリ領域の確保と解放
pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const buf = try allocator.create(u8);
    defer allocator.destroy(buf);

    // .....
}
  • std.heap.GeneralPurposeAllocator
    std.testing.allocatorの実際がこのアロケータで、ビルドモードがDebugもしくはReleaseSafeのときにメモリ領域の確保と解放のチェックを行う
GeneralPurposeAllocatorによるチェック
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    std.debug.assert(gpa.deinit() == .ok);  // エラーチェック

    const buf = try allocator.create(u8);
    _ = buf;
    // bufを開放しないとエラー
}
  • std.heap.ArenaAllocator
    別のアロケータにdeinitもしくはresetの実行によりこのアロケータで確保したメモリ領域をすべて解放できる機能を持たせるアロケータです。必ずinitで別のアロケータを設定します。
ArenaAllocatorの利用
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const allocator = arena.allocator();
defer arena.deinit();

// _ = arena.reset(.free_all);

バッファとアロケータ - 関数の定義

メモリ領域を確保する方法には、これまで紹介してきたアロケータだけでなく、あらかじめ配列で確保しておくバッファもあります。例として複数の配列の要素を1つの配列(スライス)にコピーする関数をそれぞれの方法で定義してみます。

バッファをあらかじめ用意するメリットは、メモリ領域の確保や解放の処理が不要なことです。そのかわり、バッファの容量がコピーするデータの容量とあっていないと無駄ができたり、容量が足らなかったりするのはデメリットかもしれません。ここではバッファの容量が足らなければエラーとしますが、バッファに入る分だけはコピーしたほうがよい場合もあるでしょう。

アロケータでメモリ領域を確保する場合は、関数の引数にアロケータを設定します。アロケータの型はstd.mem.Allocatorにします。これで様々なアロケータに対応できます。メモリ領域の確保は関数内で行いますが、解放は関数を呼び出した側で行います。確保するメモリ領域に無駄がないのはメリットですが、解放はプロクラムで行わなくてはならず、デバッグの手間がかかるのはデメリットかもしれません。

複数の配列の要素を1つの配列にコピー
// zig test ソースファイル名 で実行

const std = @import("std");

// 複数の配列の要素をバッファにコピー
fn concatWithBuffer(comptime T: type, buf: []T, arrays: []const []const T) !usize {
    // バッファ領域の長さを確認し、足りなければエラー
    var totalLen: usize = 0;
    for (arrays) |a| totalLen += a.len;
    if (buf.len < totalLen) return error.ShortBufferLength;

    // 配列の要素を順次バッファにコピー
    totalLen = 0;
    for (arrays) |a| {
        for (a, 0..) |v, i| {
            buf[totalLen + i] = v;
        }
        totalLen += a.len;
    }

    // コピーした要素の数を返す
    return totalLen;
}

// 複数の配列の要素をアロケータで確保した領域にコピー
fn concatWithAllocator(comptime T: type, allocator: std.mem.Allocator, arrays: []const []const T) ![]T {
    // アロケータで配列の全要素数分の領域を確保
    var totalLen: usize = 0;
    for (arrays) |a| totalLen += a.len;
    const buf = try allocator.alloc(T, totalLen);

    // 配列の要素を順次確保した領域にコピー
    totalLen = 0;
    for (arrays) |a| {
        for (a, 0..) |v, i| {
            buf[totalLen + i] = v;
        }
        totalLen += a.len;
    }

    // アロケータで確保した領域(配列、スライス)を返す
    return buf;
}

test "concat arrays" {
    const print = std.debug.print;
    const allocator = std.testing.allocator;

    // コビー元の配列
    const a1 = [_]u8{ 1, 2, 3 };
    const a2 = [_]u8{ 4, 5, 6, 7 };
    const a3 = [_]u8{ 8, 9 };

    // 要素がコピーされるバッファ
    var buf1: [15]u8 = undefined;
    // a1, a2, a3の要素をbuf1にコピー
    const totalLen = try concatWithBuffer(u8, &buf1, &.{ &a1, &a2, &a3 });
    // コピーされた要素を確認
    try std.testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }, buf1[0..totalLen]);
    print("concatWithBuffer: {any}\n", .{buf1[0..totalLen]});

    // a1, a2, a3の内容をアロケータで確保した領域にコピー
    const buf2 = try concatWithAllocator(u8, allocator, &.{ &a1, &a2, &a3 });
    // 最後にアロケータで確保した領域をを解放
    defer allocator.free(buf2);
    // コピーされた要素を確認
    try std.testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }, buf2);
    print("concatWithAllocator: {any}\n", .{buf2});
}
結果
concatWithBuffer: { 1, 2, 3, 4, 5, 6, 7, 8, 9 }
concatWithAllocator: { 1, 2, 3, 4, 5, 6, 7, 8, 9 }

バッファとアロケータ - 標準ライブラリの関数

標準ライブラリの関数でも同じ処理を行うにあたりバッファとアロケータの両方に対応しているものがあります。抜粋して紹介します。

処理 バッファ アロケータ
文字列生成 std.fmt.bufPrint std.fmt.allocPrint
ファイル読み込み(Dir) std.fs.Dir.readFile std.fs.Dir.readFileAlloc
ファイル読み込み(File) std.fs.File.readAll std.fs.File.readToEndAlloc

コマンドライン引数の取得

コマンドライン引数の取得にもアロケータが使われます。メモリ領域の確保と解放はstd.process.argsAllocstd.process.argsFreeを実行します。

コマンドライン引数の取得
// zig run ソースファイル名 -- コマンドライン引数 で実行
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    const allocator = std.heap.page_allocator;

    const args = try std.process.argsAlloc(allocator);
    for(args, 0..) |s, i| try stdout.print("arg{}: {s}\n", .{i, s});
    std.process.argsFree(allocator, args);
}
結果
# zig run ソースファイル名 -- a b c の場合
arg0: 実行ファイルのフルパス
arg1: a
arg2: b
arg3: c

まとめ

  • アロケータ(Allocator)は処理の実行中に新たなメモリ領域を確保するときと確保した領域を解放するときに使用する
  • 単独の値を格納するメモリ領域の確保はcreate、解放はdestroyで行う
  • メモリ領域の解放は確保してすぐdeferで設定しておくと忘れにくい
  • まとまったメモリ領域を確保するときはalloc、解放はfreeで行う
  • メモリ領域を増やすときはreallocで行う
  • 文字列のような番兵付きの領域はallocSentinelで確保できる
  • メモリ領域の解放忘れ、確保や解放の重複でアロケータのエラーが出る
  • 代表的なアロケータとしてstd.testing.allocator(test {...}内でのみ使用可)、std.heap.page_allocatorstd.heap.GeneralPurposeAllocatorstd.heap.ArenaAllocatorがある
  • メモリ領域をあらかじめバッファで確保するのと、アロケータで確保するのは、どちらもメリットとデメリットがあると考えられる
  • 標準ライブラリの関数でも同じ処理をするにあたりバッファとアロケータの両方に対応したものが用意されている場合がある
  • コマンドライン引数の取得にもアロケータが使われる

< ポインタとスライス(*T, *[N]T, [*]T, []T, &変数, &関数)
ざっくりZig 一覧

脚注
  1. Source Encoding ↩︎

Discussion