システムプログラミングをする上で、メモリ管理は最も重要なトピックの1つです。少ないリソースでシステムを構築するために、メモリ資源はこまめに面倒を見なくてはなりません。長期の運用にも耐えられるように、動的確保したメモリの解放忘れ (メモリリーク) が発生しないように気を使う必要もあります。
多くのプログラミング言語は、以下のいずれかの方法で動的に確保したメモリ資源を解放します。
1.はC言語が採用しているアプローチです。C言語では全ての責任はプログラマにあります。しかしプログラマが確保したメモリの解放を完璧に行うのは難しく、メモリリーク発生の温床となっていました。
2.はJavaやGoを始めとする言語が採用しているアプローチです。このような言語では、実行環境が不要となったメモリをGCにより自動的に解放します。しかし、GCによりプログラムが一時中断してしまう、実行環境が肥大化する、など、リアルタイム性が求められる組込みシステムを始め、システムプログラミングでは許容できない欠点があります。
3.はC++やRustが採用している方式です。動的確保したメモリを誰が解放するべきか、を所有権という概念により管理することで、メモリが不要になるタイミングで自動的にメモリを解放します。このメモリの自動解放は、C言語での手動解放と比較してもオーバーヘッドがありません。スマートポインタの導入により、明示的にメモリを解放しなくても、メモリリークの発生を防げるようになりました。
Zenが採用しているアプローチは、C言語同様、プログラマの責任で明示的に行う、です。Zenでは全ての動作をプログラム上に明記します。そのため、メモリの確保や解放が裏側に隠れてしまうスマートポインタのアプローチを採用していません。その代わり、メモリ解放を忘れにくくするためにdefer
/ errdefer
を言語機能として、アリーナアロケータを標準ライブラリとして、取り入れています。
もう1つ重要なこととして、ZenおよびZenの標準ライブラリにはデフォルトのメモリアロケータはありません。これはC言語がmalloc
やfree
のようなデフォルトアロケータを持っているのとは対称的です。
Zenがデフォルトアロケータを持っていない理由は単純です。Zenはランタイムを持たないように細心の注意が払われているためです。ランタイムを持っていないため、Zenのプログラムは様々な環境で変更なしに動作させることができます。OS上で動作するアプリケーションはもちろん、ベアメタルで動作するOSや組込みシステムでも同様です。
Zen標準ライブラリの中には、ArrayList
を始め、アロケータを要求するものもあります。しかし、アロケータは必須のものではありません。Zen標準ライブラリを使うにあたっては、アロケータがなくてもビルドが可能です。
Zenはデフォルトアロケータを持っていないため、動的なメモリ確保を要求する関数に対しては、明示的にアロケータを受け渡します。ここでZenのアロケータとは、Allocator
インタフェースを実装する構造体を意味します。
ここでは、Zenの標準ライブラリで用意されているアロケータの利用方法について説明します。
Zenでは複数のアロケータから利用するアロケータを選択することができます。利用可能なアロケータの種類と選択については、8章 アロケータの選択で説明します。
各アロケータは初期化方法に若干の違いがありますが、それ以外はAllocator
インタフェースを通して、共通の (std.heap
) APIを利用します。
FixedBufferAllocator
を利用する例を示します。FixedBufferAllocator
は、固定長のバッファをメモリプールとして利用するアロケータです。APIの詳細は後述しますが、heap.create
でメモリを確保し、heap.destroy
でメモリを解放しています。
examples/ch08-memory/src/memory.zen:5:18
const heap = std.heap;
test "use FixedBufferAllocator" {
// アロケータの用意
var bytes: [1024]u8 = undefined;
var allocator = heap.FixedBufferAllocator{ .buffer = bytes[0..] };
// メモリ確保 / メモリ解放
var allocated: *mut u32 = try heap.create(&mut allocator, u32);
defer heap.destroy(&mut allocator, allocated);
// 確保した領域の利用
allocated.* = 42;
ok(allocated.* == 42);
}
heap.create
では確保したメモリ領域のポインタ (正確にはエラー共用体) が返ってくるため、デリファレンスして確保した領域を利用しています。
heap.create
はメモリ不足の場合にエラーを返します。例えば、次のように4バイトしか確保していないアロケータに対して、8バイトのメモリ確保を要求すると、error.OutOfMemory
が返ってきます。
examples/ch08-memory/src/memory.zen:20:26
test "allocation failed" {
var bytes: [4]u8 = undefined;
var allocator = heap.FixedBufferAllocator{ .buffer = bytes[0..] };
var allocated = heap.create(&mut allocator, u64);
err(error.OutOfMemory, allocated);
}
標準ライブラリの中にはメモリアロケータを要求するものがいくつかあります。そのうちの1つArrayList
を利用するコードを以下に示します。ArrayList
のappend
は新しくメモリ領域を確保して、リストの一番後ろに要素を追加します。append
内でアロケータがメモリ領域の確保に失敗するとerror.OutOfMemory
が返ってきます。
examples/ch08-memory/src/memory.zen:28:40
test "use ArrayList requiring Allocator" {
const ArrayList = std.container.ArrayList;
var bytes: [1024]u8 = undefined;
var allocator = heap.FixedBufferAllocator{ .buffer = bytes[0..] };
var list = ArrayList(u32){ .allocator = &mut allocator };
defer list.deinit();
try list.append(1);
try list.append(2);
ok(list.len() == 2);
}
std.heap
のAPIの一覧を示します。T
は任意の型を、n
は任意の正数を意味します。
関数名 | 説明 |
---|---|
create | T のメモリ領域を確保します。destroy でメモリ領域を解放します。 |
destroy | create で確保されたメモリ領域を解放します。 |
alloc | T のメモリ領域をn 個分確保します。free でメモリ領域を解放します。 |
free | alloc で確保されたメモリ領域を解放します。 |
realloc | 確保済みのメモリ領域を指定したサイズで確保し直します。元のメモリにあったデータは可能な限りコピーされます。 |
shrink | 確保済みのメモリ領域を指定したサイズまで縮小します。サイズ0 への縮小はfree を呼ぶことと同等です。 |
alignedAlloc | アライメントされたT のメモリ領域をn 個分確保します。free でメモリ領域を解放します。 |
alignedRealloc | realloc に加えて、メモリアライメントの変更を要求することができます。 |
alignedShrink | shrink に加えて、に加えて、メモリアライメントの変更を要求することができます。 |
メモリ領域を確保するAPIには、create
とalloc
があります。create
はT
型のメモリを1つだけ確保し、関数の戻り値型は単一の値へのポインタ*T
です。alloc
はT
型メモリ領域を連続してn
個確保し、その関数の戻り値型はスライス[]T
です。
各APIの詳細は、以下の通りです。
pub fn create(allocator: Allocator, comptime T: type) Allocator.Error!*mut T {
pub fn destroy(allocator: Allocator, ptr: anytype) void
create
はallocator
からT
型のメモリを作成し、単一オブジェクトへのポインタを返します。確保したメモリはdestroy
で解放します。確保されたメモリの内容は初期化されていないため、必ず初期化が必要です。destroy
にはcreate
で渡したものと同じallocator
を渡す必要があります。
利用例です。
allocated = try heap.create(&mut allocator, u64);
defer heap.destroy(&mut allocator, allocated);
pub fn alloc(allocator: Allocator, comptime T: type, n: usize) Allocator.Error![]mut T {
pub fn free(allocator: Allocator, memory: anytype) void
alloc
はallocator
からT
型のメモリをn
個確保し、スライスを返します。確保したメモリはfree
で解放します。確保されたメモリの内容は初期化されていないため、必ず初期化が必要です。free
にはalloc
で渡したものと同じallocator
を渡す必要があります。
利用例です。
allocated = try heap.alloc(&mut allocator, u64, 10);
defer heap.free(&mut allocator, allocated);
注意:
free
にはalloc
で取得したスライスをそのまま渡して下さい。alloc
で取得したスライスから、部分的なスライスを作成した上で、その部分的なスライスをfree
に渡すと正常にメモリ解放処理が実施されません。
戻り値型が複雑なため、単純化しています。
pub fn realloc(allocator: Allocator, old_mem: anytype, new_n: usize)
Allocator.Error!old_memと同じアライメントと要素型を持つスライス
既に割り当てられているold_mem
のメモリサイズを変更します。メモリサイズは大きくすることも小さくすることもできます。元のメモリに格納されていたデータは可能な限りコピーされます。
戻り値型が複雑なため、単純化しています。
pub fn shrink(allocator: Allocator, old_mem: anytype, new_n: usize)
old_memと同じアライメントと要素型を持つスライス
既に割り当てられているold_mem
のメモリサイズを小さくします。new_n
が元のold_mem
のサイズより小さいことは、呼び出し側が保証しなければなりません (すなわち、プログラマの仕事です) 。
pub fn alignedAlloc(
allocator: Allocator,
comptime T: type,
/// null means naturally aligned
comptime alignment: ?u29,
n: usize,
) Allocator.Error![]align(alignment orelse @alignOf(T)) mut T {
alloc
と同様ですが、メモリアライメントを追加で指定することができます。
戻り値型が複雑なため、単純化しています。
pub fn alignedRealloc(
allocator: Allocator,
old_mem: anytype,
comptime new_alignment: u29,
new_n: usize,
) Allocator.Error![]align(new_alignment) mut old_memの要素型
realloc
と同様ですが、メモリアライメントを追加で指定することができます。
戻り値型が複雑なため、単純化しています。
pub fn alignedShrink(
allocator: Allocator,
old_mem: anytype,
comptime new_alignment: u29,
new_n: usize,
) []align(new_alignment) mut old_memの要素型
shrink
と同様ですが、メモリアライメントを追加で指定することができます。
Zenでは動的確保したメモリをどこで解放するか、はプログラマが責任を持ちます。ポインタを返す関数は、コメントで誰が所有権、すなわち、誰がメモリを解放すべきか、を書くべきです。
ただし、もう少し良い規約に従うことができます。それはAllocator
の受け渡しと所有権とを結びつける規約です。
呼び出し側がメモリの所有権を持つ場合、呼び出される関数はAllocator
を引数として受け取ります。
この違いは、例えば、std.container
のArrayList
とSinglyLinkedList
に見て取れます。
const std = @import("std");
const ArrayList = std.container.ArrayList;
test "ArrayList" {
// `allocator`は何らかのアロケータインスタンス
var list = ArrayList(u32) { .allocator = &mut allocator };
// メモリ確保が行われるが、その所有権は`list`にある
try list.append(1);
}
const SinglyLinkedList = std.container.SinglyLinkedList;
test "SinglyLinkedList" {
var list = SinglyLinkedList(u32) {};
// `allocator`は何らかのアロケータインスタンス
// メモリ確保が行われるが、その所有権は呼び出しているこの関数にある
var one = try list.createNode(1, &mut allocator);
list.append(one);
}
☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.