ざっくりZig - アロケータの使い方
アロケータ(Allocator)は処理の実行中に新たなメモリ領域を確保するときと確保した領域を解放するときに使用します。var buf: [3]u8 = undefined;
のようにあらかじめ確保されたバッファ領域はアロケータの対象になりません。
まずはテスト用のアロケータであるstd.testing.allocatorを直接使ってアロケータの基本的な操作をしてみます。このアロケータはtest {...}
の部分でしか使用できません。
単独の値を格納するメモリ領域の確保と解放
u8
型をはじめ、単独の値を格納するメモリ領域の確保はcreate(型名)
で行います。create
の戻り値はポインタで、メモリを確保できないときはerror.OutOfMemory
が返されます。そして確保したメモリ領域の解放はdestroy(変数名)
で行います。引数はcreate
で返されたポインタを持つ変数です。destroy
はcreate
のすぐ後に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
シンプルなアロケータでチェックはなし
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
のときにメモリ領域の確保と解放のチェックを行う
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
で別のアロケータを設定します。
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
にします。これで様々なアロケータに対応できます。メモリ領域の確保は関数内で行いますが、解放は関数を呼び出した側で行います。確保するメモリ領域に無駄がないのはメリットですが、解放はプロクラムで行わなくてはならず、デバッグの手間がかかるのはデメリットかもしれません。
// 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.argsAllocとstd.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_allocator、std.heap.GeneralPurposeAllocator、std.heap.ArenaAllocatorがある - メモリ領域をあらかじめバッファで確保するのと、アロケータで確保するのは、どちらもメリットとデメリットがあると考えられる
- 標準ライブラリの関数でも同じ処理をするにあたりバッファとアロケータの両方に対応したものが用意されている場合がある
- コマンドライン引数の取得にもアロケータが使われる
< ポインタとスライス(*T, *[N]T, [*]T, []T, &変数, &関数)
structは構造型、型はtype、typeは値 >
ざっくりZig 一覧
Discussion