オフィスアワーがそろそろ始まるよ!()

メモリ管理

システムプログラミングをする上で、メモリ管理は最も重要なトピックの1つです。少ないリソースでシステムを構築するために、メモリ資源はこまめに面倒を見なくてはなりません。長期の運用にも耐えられるように、動的確保したメモリの解放忘れ (メモリリーク) が発生しないように気を使う必要もあります。

多くのプログラミング言語は、以下のいずれかの方法で動的に確保したメモリ資源を解放します。

  1. プログラマの責任で明示的に行う
  2. 実行環境がガベージコレクション (GC) により自動的に解放する
  3. スマートポインタで所有権を管理し、メモリが不要になるタイミングで自動的に解放する

1.はC言語が採用しているアプローチです。C言語では全ての責任はプログラマにあります。しかしプログラマが確保したメモリの解放を完璧に行うのは難しく、メモリリーク発生の温床となっていました。

2.はJavaやGoを始めとする言語が採用しているアプローチです。このような言語では、実行環境が不要となったメモリをGCにより自動的に解放します。しかし、GCによりプログラムが一時中断してしまう、実行環境が肥大化する、など、リアルタイム性が求められる組込みシステムを始め、システムプログラミングでは許容できない欠点があります。

3.はC++やRustが採用している方式です。動的確保したメモリを誰が解放するべきか、を所有権という概念により管理することで、メモリが不要になるタイミングで自動的にメモリを解放します。このメモリの自動解放は、C言語での手動解放と比較してもオーバーヘッドがありません。スマートポインタの導入により、明示的にメモリを解放しなくても、メモリリークの発生を防げるようになりました。

Zenが採用しているアプローチは、C言語同様、プログラマの責任で明示的に行う、です。Zenでは全ての動作をプログラム上に明記します。そのため、メモリの確保や解放が裏側に隠れてしまうスマートポインタのアプローチを採用していません。その代わり、メモリ解放を忘れにくくするためにdefer / errdeferを言語機能として、アリーナアロケータを標準ライブラリとして、取り入れています。

もう1つ重要なこととして、ZenおよびZenの標準ライブラリにはデフォルトのメモリアロケータはありません。これはC言語がmallocfreeのようなデフォルトアロケータを持っているのとは対称的です。

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: *u32 = try heap.create(&allocator, u32);
    defer heap.destroy(&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(&allocator, u64);
    err(error.OutOfMemory, allocated);
}

標準ライブラリの中にはメモリアロケータを要求するものがいくつかあります。そのうちの1つArrayListを利用するコードを以下に示します。ArrayListappendは新しくメモリ領域を確保して、リストの一番後ろに要素を追加します。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 = &allocator };
    defer list.deinit();

    try list.append(1);
    try list.append(2);

    ok(list.len() == 2);
}

アロケータAPIリファレンス

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には、createallocがあります。createT型のメモリを1つだけ確保し、関数の戻り値型は単一の値へのポインタ*Tです。allocT型メモリ領域を連続してn個確保し、その関数の戻り値型はスライス[]Tです。

各APIの詳細は、以下の通りです。

create / destroy

pub fn create(allocator: Allocator, comptime T: type) Allocator.Error!*T
pub fn destroy(allocator: Allocator, ptr: var) void

createallocatorからT型のメモリを作成し、単一オブジェクトへのポインタを返します。確保したメモリはdestroyで解放します。確保されたメモリの内容は初期化されていないため、必ず初期化が必要です。destroyにはcreateで渡したものと同じallocatorを渡す必要があります。

利用例です。

    allocated = try heap.create(&allocator, u64);
    defer heap.destroy(&allocator, allocated);

alloc / free

pub fn alloc(allocator: Allocator, comptime T: type, n: usize) Allocator.Error![]T
pub fn free(allocator: Allocator, memory: var) void

allocallocatorからT型のメモリをn個確保し、スライスを返します。確保したメモリはfreeで解放します。確保されたメモリの内容は初期化されていないため、必ず初期化が必要です。freeにはcreateで渡したものと同じallocatorを渡す必要があります。

利用例です。

    allocated = try heap.alloc(&allocator, u64, 10);
    defer heap.free(&allocator, allocated);

注意: freeにはallocで取得したスライスをそのまま渡して下さい。allocで取得したスライスから、部分的なスライスを作成した上で、その部分的なスライスをfreeに渡すと正常にメモリ解放処理が実施されません。

realloc

戻り値型が複雑なため、単純化しています。

pub fn realloc(allocator: Allocator, old_mem: var, new_n: usize) 
    Allocator.Error!old_memと同じアライメントと要素型を持つスライス
}

既に割り当てられているold_memのメモリサイズを変更します。メモリサイズは大きくすることも小さくすることもできます。元のメモリに格納されていたデータは可能な限りコピーされます。

shrink

戻り値型が複雑なため、単純化しています。

pub fn shrink(allocator: Allocator, old_mem: var, new_n: usize)
    old_memと同じアライメントと要素型を持つスライス

既に割り当てられているold_memのメモリサイズを小さくします。new_nが元のold_memのサイズより小さいことは、呼び出し側が保証しなければなりません (すなわち、プログラマの仕事です) 。

alignedAlloc

pub fn alignedAlloc(
    allocator: Allocator,
    comptime T: type,
    comptime alignment: u29,
    n: usize,
) Allocator.Error![]align(alignment) T

allocと同様ですが、メモリアライメントを追加で指定することができます。

alignedRealloc

戻り値型が複雑なため、単純化しています。

pub fn alignedRealloc(
    allocator: Allocator,
    old_mem: var,
    comptime new_alignment: u29,
    new_n: usize,
) Allocator.Error![]align(new_alignment) old_memの要素型

reallocと同様ですが、メモリアライメントを追加で指定することができます。

alignedShrink

戻り値型が複雑なため、単純化しています。

pub fn alignedShrink(
    allocator: Allocator,
    old_mem: var,
    comptime new_alignment: u29,
    new_n: usize,
) []align(new_alignment) old_memの要素型

shrinkと同様ですが、メモリアライメントを追加で指定することができます。

所有権の考え方

Zenでは動的確保したメモリをどこで解放するか、はプログラマが責任を持ちます。ポインタを返す関数は、コメントで誰が所有権、すなわち、誰がメモリを解放すべきか、を書くべきです。

ただし、もう少し良い規約に従うことができます。それはAllocatorの受け渡しと所有権とを結びつける規約です。

呼び出し側がメモリの所有権を持つ場合、呼び出される関数はAllocatorを引数として受け取ります。

この違いは、例えば、std.containerArrayListSinglyLinkedListに見て取れます。

const std = @import("std");

const ArrayList = std.container.ArrayList;
test "ArrayList" {
    // `allocator`は何らかのアロケータインスタンス
    var list = ArrayList(u32) { .allocator = &allocator };

    // メモリ確保が行われるが、その所有権は`list`にある
    try list.append(1);
}

const SinglyLinkedList = std.container.SinglyLinkedList;
test "SinglyLinkedList" {
    var list = SinglyLinkedList(u32) {};

    // `allocator`は何らかのアロケータインスタンス
    // メモリ確保が行われるが、その所有権は呼び出しているこの関数にある
    var one = try list.createNode(1, &allocator);
    list.append(one);
}

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 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.