D 1.0   D 2.0
About Japanese Translation

Last update Fri May 16 23:03:05 2008

メモリ管理

少しでも大きなプログラムなら、 ほぼ確実に、メモリ割り当てと解放が必要になります。 プログラムが複雑に、大きくなるにつれ、メモリ管理技術はより一層重要になります。 Dは、メモリ管理の様々なオプションを提供します。

Dでメモリを割り当てる簡便な方法とは次の3つです:

  1. 静的データ。デフォルトのデータセグメントに割り当てられる。
  2. スタックデータ。CPUのプログラムスタックへ割り当てられる。
  3. ガベージコレクトされたデータ。 GCのヒープから動的に割り当てられる。

この章では、これらを使うテクニックと、 より高度な代替手段について説明します。

文字列 (と 配列) の Copy-on-Write

配列を受け取って、それを(もしかしたら)変更して返す関数を考えてみましょう。 配列は値ではなく参照渡しされますので、 "この配列の内容の所有者は誰だ?" という重大な問題が持ち上がります。 例えば、配列の内容を大文字に変換する関数:

char[] toupper(char[] s)
{
    int i;

    for (i = 0; i < s.length; i++)
    {
	char c = s[i];
	if ('a' <= c && c <= 'z')
	    s[i] = c - (cast(char)'a' - 'A');
    }
    return s;
}

呼び出し側のs[]も変更されることに注意してください。これは、 意図しない動作かもしれません。あるいはもっと悪く、s[] はメモリの読み取り専用領域のスライスかもしれません。

しかし、toupper() で常に s[] のコピーを作ると、 既に文字列が大文字だった場合には、 無駄にメモリと時間を消費してしまいます。

解決策は copy-on-write、 つまり文字列を変更する必要があるときだけコピーする、 方式で実装することです。いくつかの文字列処理言語では、 これが実際にデフォルトの挙動となっていますが、これは大きなコストがかかっています。 文字列 "abcdeF" は関数によって5回コピーされることになるでしょう。 最大に効率的に実装するには、copy-on-write を明示的にコードとして書くことです。 以下に、効率的にcopy-on-writeを実装して書き直したtoupperを示します。

char[] toupper(char[] s)
{
    int changed;
    int i;

    changed = 0;
    for (i = 0; i < s.length; i++)
    {
	char c = s[i];
	if ('a' <= c && c <= 'z')
	{
	    if (!changed)
	    {   char[] r = new char[s.length];
		r[] = s;
		s = r;
		changed = 1;
	    }
	    s[i] = c - (cast(char)'a' - 'A');
	}
    }
    return s;
}

D の Phobos ランタイムライブラリでは、配列処理関数は copy-on-write で実装されています。

リアルタイム

リアルタイムプログラミングでは、プログラムは、遅れ時間の上限か 操作完了にかかる時間の保証をしなくてはなりません。ほとんどの メモリ割り当て操作、これはmalloc/freeやGCを含みますが、には、 理論的な時間の上限はありません。 レイテンシを保証する最も信頼性のある方法は、 時間制限の厳しい箇所で必要なメモリは全てあらかじめ割り当てておく、 という手です。もしそこにメモリ割り当てのコードがなければ、 GCは走らず、時間制限を超過してしまう原因にもなりません。

スムーズな操作

リアルタイムプログラミングに関連した事項として、 プログラムがGCによって任意の時点で動作中断することなく、 スムーズに切れ目無く動作することが必要な場合があります。 このようなプログラムの例としては、ガンシューティング・ゲームが上げられます。 ゲームが不規則に中断するのは、プログラムにとっては致命的ではありませんが、 ユーザーにとっては迷惑なことでしょう。

いくつか、この挙動を和らげるテクニックがあります:

フリーリスト

フリーリストは、頻繁に割り当て/破棄が行われる型へのアクセスを高速化する 強力な手法です。考え方は簡単で、使い終わったオブジェクトは、解放せずに フリーリストへ積んで置くというだけです。割り当て時は、 まずフリーリストのオブジェクトを1つ取り出してきて使います。

class Foo
{
    static Foo freelist;		// フリーリストの先頭

    static Foo allocate()
    {   Foo f;

	if (freelist)
	{   f = freelist;
	    freelist = f.next;
	}
	else
	    f = new Foo();
	return f;
    }

    static void deallocate(Foo f)
    {
	f.next = freelist;
	freelist = f;
    }

    Foo next;		// FooFreeList で使うため
    ...
}

void test()
{
    Foo f = Foo.allocate();
    ...
    Foo.deallocate(f);
}
このようなフリーリスト方式で、高いパフォーマンスを得ることも可能です。

参照カウント

参照カウントとは、オブジェクトにカウント変数を含める、 ということです。 オブジェクトを指す参照が増えればカウントを増やし、参照が終われば カウントを減らします。カウントが0になったオブジェクトは削除できます。

D は参照カウントを自動化する方法を提供していません。 明示的に記述する必要があります。

Win32 COM プログラミング では、参照カウントの管理に、 メンバ AddRef() と Release() を使います。

明示的なクラスインスタンス割り当て

D は、クラスのインスタンスのためのカスタムアロケータ/デアロケータを 作る方法を提供しています。通常、インスタンスはGCのヒープから確保され、 GCが走ったときに解放されます。特定の目的に対しては、 NewDeclarationDeleteDeclaration を書いてメモリ割り当てを管理することができます。 例えば、Cのランタイム関数 mallocfree で割り当てを行う例です:

import std.c.stdlib;
import std.outofmemory;
import std.gc;

class Foo
{
    new(size_t sz)
    {
	void* p;

	p = std.c.stdlib.malloc(sz);
	if (!p)
	    throw new OutOfMemoryException();
	std.gc.addRange(p, p + sz);
	return p;
    }

    delete(void* p)
    {
	if (p)
	{   std.gc.removeRange(p);
	    std.c.stdlib.free(p);
	}
    }
}

new() の重要な特徴は:

deleteの重要な特徴は:

クラス特有のアロケータを使ってメモリを割り当てる場合は、 '宙ぶらりんポインタ' や 'メモリリーク' が発生しないように 注意深くコードを書かなくてはなりません。例外の存在まで考慮すると、 メモリリークを防ぐためには RAII を使う習慣が重要です。

構造体や共用体についても、 カスタムアロケータを定義して使うすることができます。

Mark/Release

Mark/Release は、スタックを使うのと同等のメモリ割り当て/解放方法です。 「スタック」はメモリ中に作られます。オブジェクトは、メモリ中でポインタを 下に動かすことで作成します。個々のポイントが'mark'され、 その点へポインタを戻すことで、それまでに確保されたオブジェクトを 'release' します。

import std.c.stdlib;
import std.outofmemory;

class Foo
{
    static void[] buffer;
    static int bufindex;
    static const int bufsize = 100;

    static this()
    {   void *p;

	p = malloc(bufsize);
	if (!p)
	    throw new OutOfMemoryException;
	std.gc.addRange(p, p + bufsize);
	buffer = p[0 .. bufsize];
    }

    static ~this()
    {
	if (buffer.length)
	{
	    std.gc.removeRange(buffer);
	    free(buffer);
	    buffer = null;
	}
    }

    new(size_t sz)
    {   void *p;

	p = &buffer[bufindex];
	bufindex += sz;
	if (bufindex > buffer.length)
	    throw new OutOfMemory;
	return p;
    }

    delete(void* p)
    {
	assert(0);
    }

    static int mark()
    {
	return bufindex;
    }

    static void release(int i)
    {
	bufindex = i;
    }
}

void test()
{
    int m = Foo.mark();
    Foo f1 = new Foo;		// 割り当て
    Foo f2 = new Foo;		// 割り当て
    ...
    Foo.release(m);		// f1 と f2 を解放
}

割り当て用の buffer[] それ自身は、 GC 対象領域に追加されます。 このため、Foo.new() でその作業をするコードは必要ありません。

RAII (Resource Acquisition Is Initialization)

明示的なアロケータ/デアロケータを使っているときは、 メモリリークを避けるためにRAIIというテクニックが有効です。 そのようなクラスには scope属性 を加えると良いでしょう。

スタックへのクラスインスタンス割り当て

クラスのインスタンスは、通常はガベージコレクタに管理された ヒープに割り当てられます。しかし、以下の条件が満たされていれば…

…インスタンスはスタックに割り当てられます。 これはインスタンスのallocate/freeを行うよりも効率的ですが、 オブジェクトへの参照が関数からのreturn後も生き延びることのないよう、 注意が必要です。

class C { ... }

scope c = new C();	// c はスタックに割り当てられる
scope c2 = new C(5);    // スタックに割り当てられる
scope c3 = new(5) C();  // カスタムアロケータで割り当てられる

クラスにデストラクタが定義されていた場合、 そのデストラクタが、オブジェクトがスコープを抜けるタイミングで呼ばれることが保証されます。 例外でスコープを抜けるときも同様です。

スタックへの未初期化配列の割り当て

Dでは、配列は常に初期化されます。例えば、以下の宣言:

void foo()
{   byte[1024] buffer;

    fillBuffer(buffer);
    ...
}

は、最初にbuffer[]の中身を初期化するため、 あなたが思うほどは速くないかもしれません。もし注意深くプロファイルを取った結果、 この初期化がスピードに関して問題になっているとわかったならば、 Void初期化子によってこれを除去することができます:

void foo()
{   byte[1024] buffer = void;

    fillBuffer(buffer);
    ...
}

スタック上の未初期化データには、 使用する前によく考えておかねばならない危険が伴います:

割り込みサービスルーチン

ガベージコレクタがメモリ回収を実行するときには、 レジスタやスタックの内容を調べてGCで割り当てたオブジェクトへの参照を集めるために、 いったん全てのスレッドの実行を停止する必要があります。 しかし、ISR (Interrupt Service Routine / 割り込みサービスルーチン) スレッドを停止してしまうと、プログラムの動きがおかしくなってしまいます。

このため、ISR スレッドは停止すべきではありません。 標準ライブラリ std.thread の関数で作ったスレッドは停止しますが、Cの _beginthread() やそれと同等のルーチンで作ったスレッドは、 GCがその存在を知らないため、停止しません。

これを使って適切な動作を得るためには: