[ZIG] 各アロケータの解説
はじめに
皆さん、zig使ってますか!?zigはよくrustやc3、jaiなどと比べられますが個人的にはどの言語も長所や短所があると思います。そして、zigの良い点である要素にメモリ管理の自由度があげられます。ただ、自由度が高いということは学習難易度が比較的高いというデメリットとトレードオフです。実のところ自身も完全な把握は出来ていません。間違えている箇所がある場合はコメント等で教えてほしいです。今回使用するzigのバージョンは0.14.0-dev.2563+af5e73172です。
アロケータの種類
General Purpose Allocator
汎用アロケータです、速度よりも安全性を重視していて二重開放を検知する機能とメモリリークを検知する機能を持っています。構造体で指定することで、安全性チェックとスレッドセーフティーをオフに出来ます。二重開放の検知はそもそもzigの言語仕様で、os保護機能によるセグメントフォルトが発生するためGPAでいうところの二重開放を検知する機能は、gpa内部でメモリブロックにヘッダを付け開放時にマークすることで内部メカニズムとしての二重開放検知という解釈だと思います。
自身の環境ではgpaの二重開放検知機能よりも先にセグメントフォルトが発生してしまい再現が出来ませんでした。
- メモリリークテスト
test "gpa double free check with defer" {
var gpa = std.heap.GeneralPurposeAllocator(.{}).init;
const allocator = gpa.allocator();
defer {
const result = gpa.deinit();
if (result == .leak) {
std.debug.print("Memory leak detected\n", .{});
}
if (result == .ok) {
std.debug.print("Memory deallocated\n", .{});
}
}
// deferで開放していなことによるリーク
_ = try allocator.alloc(u8, 100);
}
このコードではメモリリークエラーが発生すると思います。しっかりと機能していますね。ちなみにdeferは上記の様にブロックを使用してクリーンアップコードと言われる、スコープを抜ける時に必ず実行される処理を記述できます。
PageAllocator
PageAllocatorは呼び出された時にosにメモリの全ページを要求します。単一のバイト割当に複数のキビバイトを要求する可能性が高いです。そしてosにシステムコールを介してメモリの要求をするため単一のバイトに複数のキビバイトを要求する点とosにシステムコールを介してメモリの要求をする点、この2つの理由から小さなメモリ割り当てをする場合、メモリ使用量と速度の両方で非効率な設計になっています。(とはいえgpaよりは高速)逆に大きなメモリ割り当てをする場合は設計がシンプルな点gpaのようなメタデータが存在しないためオーバーヘッドが少ないので高速になります。
- メモリリークテスト
test "page allocator memory leak check" {
const allocator = std.heap.page_allocator;
// メモリリークが起きる
_ = try allocator.alloc(u8, 1000);
}
この様なコードで意図的にメモリリークを発生させた場合、gpaではパニックを発生させることが出来ましたがpage allocatorでは問題なくビルドできてしまいました。そもそもgpaのようにdenit();
関数が無いためリークを検知することが出来ません。そう考えるとやはり遅くても重要なコードではgpaを使用するのが良いでしょう。
defer {
const result = gpa.deinit();
if (result == .leak) {
std.debug.print("Memory leak detected\n", .{});
}
if (result == .ok) {
std.debug.print("Memory deallocated\n", .{});
}
}
FixedBufferAllocator
FixedBufferAllocatorは固定バッファーをアロケータに割り当てるアロケータです。
ヒープ割当を使用しないので速度は早いです。固定バッファーということですのでサイズを再割り当てなどは出来ず使用できる範囲を超えるとOutOfMemory
を発生させます。
- メモリリークテスト
test "fixed buffer allocator memory leak check" {
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
_ = try allocator.alloc(u8, 8);
// try allocator.alloc(u8,1025); OutOfMemory
}
もちろんメモリリークは検知出来ません。
ArenaAllocator
ArenaAllocatorは子要素のアロケータを受け取り何度も割当をすることが出来ます。開放するのは一回のみで全ての子要素のアロケータを開放します。
- メモリリークテスト
test "arena allocator memory leak check" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const allocator = arena.allocator();
_ = try allocator.alloc(u8, 8);
_ = try allocator.alloc(u8, 16);
_ = try allocator.alloc(u8, 32);
}
結果はgpa以外と同じです。Arenaを使う時は子要素のアロケータで操作をするということはなるべく避け、必ずarena.deinit();
を呼び出すように気をつければ良いと思います。
C_Allocator
C_AllocatorはC言語と互換があるアロケータです。mallocやfreeを直接使用したい場合はraw_c_allocatorというアロケータを使用する必要があります。使用方法は他のgpa以外のアロケータとさほど変わりません。
- テストコード
test "c_allocator" {
var c = std.heap.c_allocator;
const memory = try c.alloc(u8, 10);
try std.testing.expect(memory.len == 10);
defer c.free(memory);
std.debug.print("c_allocator memory leak check {d}\n", .{memory.len});
}
そして使用するにはLibCと連携する必要があります。build.zigにリンクするように書きましょう。
- build.zig
const exe_unit_tests = b.addTest(.{ .root_module = exe_mod, .optimize = .Debug });
exe_unit_tests.linkLibC();
これで起動するはずです。
簡易ベンチマーク
各アロケータのベンチマークを簡易的に行ってみました。
与えたバイト数はこちらです。
const small_size = 8;
const medium_size = 1024;
const large_size = 20480;
結果はこちら
PageAllocator | FixedBufferAllocator | ArenaAllocator | C_Allocator | GPA |
---|---|---|---|---|
2307ns | 257ns | 1874ns | 4419ns | 21242ns |
1041ns | 132ns | 307ns | 190ns | 9860ns |
2775ns | 278ns | 3420ns | 2405ns | 11476ns |
最後に
今回は自分で調べたzigのアロケータを書いてみました。まだ説明していないアロケータでwasmアロケータなどがありますが、将来gpaと統合されると書かれているのでそうなってから追記するかもです。zigは公式ドキュメントよりもこのサイトの方が分かりやすく作ってる人に感謝です。そんな公式もメモリの使用についてYoutubeで動画を上げているので勉強になりました。そしてzigは最初はエラーばっかで難しいと思うかもしれませんが凄く良い言語だと思うので利用者がもっと増えてほしいナと個人的に思います。
Discussion