メモリ管理

Dは、明示的なメモリ管理を必要としない言語だ。しかし、システムプログラマとしては、特別な場合にメモリを管理する方法を知っておくことが重要だ。

メモリ管理は非常に広範なトピックだ。この章では、ガベージコレクタ(GC)、GCからメモリを割り当てる方法、および特定のメモリ位置にオブジェクトを構築する方法についてのみ説明する。この本を書いた時点ではまだ実験段階だったstd.allocatorモジュールだけでなく、さまざまなメモリ管理手法についても調べてみることをお勧めする。

前の章と同様、以下で"変数"と表記する場合は、structおよびclassオブジェクトを含むあらゆる型の変数を指す。

メモリ

メモリは、実行中のプログラムとそのデータの両方がメモリ内に格納されるため、他のシステムリソースよりも重要なリソースだ。メモリは、最終的にはオペレーティングシステムに属しており、プログラムがそのニーズに応じて使用できるようにしている。プログラムが使用するメモリの量は、そのプログラムの当面のニーズに応じて増減する。プログラムが終了すると、そのプログラムが使用していたメモリ領域は自動的にオペレーティングシステムに戻される。

メモリは、変数の値が書き込まれた大きな紙のようなものと考えることができる。各変数は、その値が必要に応じて書き込まれたり読み出されたりする特定の場所に保持される。変数の寿命が終了すると、その場所は別の変数に使用される。

メモリを実験する場合、&(アドレス)演算子が便利だ。例えば、次のプログラムは、隣り合って定義されている2つの変数のアドレスを出力する。

import std.stdio;

void main() {
    int i;
    int j;

    writeln("i: ", &i);
    writeln("j: ", &j);
}

注釈:プログラムを実行するたびに、アドレスは異なる値になる可能性が高い。さらに、変数のアドレスを取得するだけで、その変数をCPUレジスタに保存する最適化が無効になる。

出力からわかるように、変数の位置は4バイト離れている:

i7FFF2B633E28
j7FFF2B633E2C

2つのアドレスの最後の桁は、ijの場所のすぐ前のメモリ位置に存在することを示している。8に4(intのサイズ)を加えると12(16進表記のC)になる。

ガベージコレクタ

Dプログラムで使用される動的変数は、ガベージコレクタ(GC)が所有するメモリブロック上に存在する。変数の寿命が終了すると (つまり、その変数が使用されなくなった場合)、その変数はGCによって実行されるアルゴリズムに従って最終化される。その変数を含むメモリ位置を他に必要とするものがない場合、そのメモリは他の変数に使用するために再利用される。このアルゴリズムはガベージコレクションと呼ばれ、その実行はガベージコレクションサイクルと呼ばれる。

GCが実行するアルゴリズムは、おおまかに次のように説明できる。プログラムのルートにあるポインタ(参照を含む)によって直接的または間接的に到達可能なすべてのメモリブロックがスキャンされる。到達可能なメモリブロックは"まだ使用中"とタグ付けされ、それ以外のメモリブロックは"使用済み"とタグ付けされる。アクセスできないブロックにあるオブジェクトおよび構造体のファイナライザが実行され、それらのメモリブロックは将来の変数に使用するために再利用される。ルートは、すべてのスレッドのプログラムスタック、すべてのグローバル変数およびスレッドローカル変数、およびGC.addRootまたはGC.addRangeによって追加された追加データとして定義される。

一部のGCアルゴリズムは、オブジェクトをメモリ内の同じ場所にまとめるために移動させることがある。プログラムの正しさを保つため、そのようなオブジェクトを指すすべてのポインタ(および参照)は、自動的に新しい位置を指すように変更される。Dの現在のGCはこれを行わない。

GCが"正確"であるとは、どのメモリがポインタを含むかを含まないかを正確に知っていることを意味する。GCが保守的であるとは、すべてのメモリをポインタとしてスキャンすることを意味する。DのGCは部分的に保守的で、ポインタを含むブロックのみをスキャンするが、それらのブロック内のすべてのデータをスキャンする。このため、一部のブロックは収集されず、そのメモリが"リーク"する可能性がある。大きなブロックは"偽のポインタ"の対象になりやすい。この問題を回避するため、使用しなくなった大きなブロックを手動で解放することをおすすめする場合がある。

ファイナライザの実行順序は不特定だ。例えば、オブジェクトの参照メンバーは、そのメンバーを含むオブジェクトよりも先にファイナライズされる場合がある。そのため、動的変数を参照するクラスメンバーは、デストラクタ内でアクセスしてはならない。これは、C++などの言語の決定的な破棄順序とは大きく異なることに注意しよう。

ガベージコレクションサイクルは、より多くのデータ用のスペースを見つける必要があるなど、さまざまな理由で開始される。GCの実装によっては、ガベージコレクションサイクル中に新しいオブジェクトを割り当てると、コレクションプロセス自体が妨害される可能性があるため、コレクションサイクル中は実行中のすべてのスレッドを停止しなければならない場合がある。これは、プログラムの実行の遅延として観察されることがある。

ほとんどの場合、プログラマはガベージコレクションプロセスに干渉する必要はない。ただし、core.memoryモジュールで定義された関数を使用して、必要に応じてガベージコレクションサイクルを遅延またはディスパッチすることは可能だ。

ガベージコレクションサイクルの開始と遅延

プログラムの応答性が重要な部分では、ガベージコレクションサイクルの実行を遅らせたい場合がある。GC.disableはガベージコレクションサイクルを無効にし、GC.enableは再び有効にする。

GC.disable();

// ... 応答性が重要なプログラムの一部 ...

GC.enable();
D

ただし、GC.disableによってガベージコレクションサイクルの実行が確実に防止されるわけではない。GCがOSからより多くのメモリを取得する必要があるが、取得できない場合、GCは、利用可能なメモリを確保するための最後の手段として、ガベージコレクションサイクルを実行する。

不特定の時間に自動的に実行されるガベージコレクションに頼る代わりに、GC.collect()を使用してガベージコレクションサイクルを明示的に開始することができる。

import core.memory;

// ...

    GC.collect();    // ガベージコレクションサイクルを開始する
D

通常、GCはメモリブロックをオペレーティングシステムに返さない。プログラムが将来必要とする場合に備えて、それらのメモリページを保持する。必要に応じて、GC.minimize()を使用して、GCに未使用のメモリをオペレーティングシステムに返すよう依頼することができる。

GC.minimize();
D
メモリの割り当て

システム言語では、オブジェクトが格納されるメモリ領域を指定することができる。このようなメモリ領域は、一般にバッファと呼ばれる。

メモリを割り当てる方法はいくつかある。最も簡単な方法は、固定長配列を使用することだ:

ubyte[100] buffer;    // 100バイトのメモリ領域
D

bufferは、100バイトのメモリ領域として使用できる状態になっている。ubyteの代わりに、型と関連付けずに、voidの配列としてこのようなバッファを定義することもできる。voidには値を割り当てることができないため、.init値も持つことができない。このような配列は、特別な構文=void

void[100] buffer = void;    // 100バイトのメモリ領域
D

この章では、メモリを予約するためにcore.memoryモジュールからGC.callocだけを使う。このモジュールには、さまざまな場面で役立つ他の機能も多数ある。さらに、C標準ライブラリのメモリ割り当て関数は、core.stdc.stdlibモジュールでも使える。

GC.calloc 指定したサイズのメモリ領域を、すべて0の値で事前に入力して割り当て、割り当てた領域の開始アドレスを返す。

import core.memory;
// ...
    void * buffer = GC.calloc(100);
                            // 100バイトのゼロのメモリ領域
D

通常、返されるvoid*の値は、適切な型のポインタにキャストされる。

int * intBuffer = cast(int*)buffer;
D

しかし、通常はその中間ステップは省略され、戻り値は直接キャストされる。

int * intBuffer = cast(int*)GC.calloc(100);
D

100のような任意の値ではなく、通常、メモリ領域のサイズは、必要な要素の数と各要素のサイズを乗算して計算される。

// 25個のintの領域を確保する
int * intBuffer = cast(int*)GC.calloc(int.sizeof * 25);
D

クラスには重要な違いがある。クラス変数のサイズとクラスオブジェクトのサイズは同じではない。.sizeofはクラス変数のサイズで、常に同じ値になる。64ビットシステムでは8、32ビットシステムでは4だ。クラスオブジェクトのサイズは、__traits(classInstanceSize)で取得する必要がある。

// 10個のMyClass オブジェクト用の領域を確保する
MyClass * buffer =
    cast(MyClass*)GC.calloc(
        __traits(classInstanceSize, MyClass) * 10);
D

要求されたサイズをシステムに十分なメモリがない場合、core.exception.OutOfMemoryError例外がスローされる。

void * buffer = GC.calloc(10_000_000_000);
D

そのくらいの空き容量がないシステムでの出力:

core.exception.OutOfMemoryError

GCから割り当てられたメモリ領域は、GC.freeを使用してGCに返すことができる:

GC.free(buffer);
D

ただし、free()を呼び出しても、そのメモリブロック上に存在する変数のデストラクタが必ず実行されるとは限らない。デストラクタは、各変数に対してdestroy()を呼び出すことで明示的に実行することができる。GCコレクションまたは解放中に、classおよびstruct変数のファイナライザを呼び出すには、さまざまな内部メカニズムが使用されることに注意しよう。これらを確実に呼び出す最善の方法は、変数を割り当てる際にnew演算子を使用することだ。その場合、GC.freeがデストラクタを呼び出す。

プログラムは、以前に割り当てられたメモリ領域がすべて使用済みで、追加のデータ格納スペースがないと判断する場合がある。GC.reallocを使用すると、以前に割り当てられたメモリ領域を拡張できる。realloc()は、以前に割り当てられたメモリポインタと新たに要求されたサイズを受け取り、新しい領域を返す:

void * oldBuffer = GC.calloc(100);
// ...
void * newBuffer = GC.realloc(oldBuffer, 200);
D

realloc() 必要最小限のメモリを割り当てるように効率化されている:

GC.reallocは、C標準ライブラリの関数realloc()を応用したものだ。このような複雑な動作をするため、realloc()は関数インターフェースの設計が悪いとされている。GC.reallocの意外な点は、元のメモリがGC.callocで割り当てられていた場合でも、拡張部分は決してクリアされないことだ。そのため、メモリをゼロ初期化することが重要な場合は、以下のような関数reallocCleared()が便利だ。blockAttributesの意味については、後で説明する。

import core.memory;

/* GC.reallocと同じように動作するが、メモリが拡張された場合、
 * 余分なバイトをクリアする。 */
void * reallocCleared(
    void * buffer,
    size_t oldLength,
    size_t newLength,
    GC.BlkAttr blockAttributes = GC.BlkAttr.NONE,
    const TypeInfo typeInfo = null) {
    /* 実際の処理をGC.reallocに委譲する。 */
    buffer = GC.realloc(buffer, newLength,
                        blockAttributes, typeInfo);

    /* 拡張された場合、余分なバイトをクリアする。 */
    if (newLength > oldLength) {
        import core.stdc.string;

        auto extendedPart = buffer + oldLength;
        const extendedLength = newLength - oldLength;

        memset(extendedPart, 0, extendedLength);
    }

    return buffer;
}

上記の関数は、core.stdc.stringモジュールにあるmemset()を使用して、新たに拡張されたバイトをクリアしている。memset()は、ポインタと長さで指定したメモリ領域のバイトに、指定した値を代入する。この例では、extendedPartextendedLengthバイトに0を代入している。

以下の例でreallocCleared()を使う。

同様の関数であるGC.extendの動作は、realloc()ほど複雑ではなく、上記の最初の項目のみを適用する。つまり、メモリ領域をその場で拡張できない場合、extend()は何も行わず、0を返す。

メモリブロックの属性

GCアルゴリズムの概念と手順は、enum BlkAttrによってメモリブロックごとにある程度設定することができる。BlkAttrは、GC.callocおよびその他の割り当て関数のオプションパラメータだ。このパラメータは、以下の値で構成される。

enum BlkAttrの値は、ビット演算の章で見たビットフラグとして使用するのに適している。|演算子によって2つの属性をマージする方法は、次の通りだ。

const attributes =
    GC.BlkAttr.NO_SCAN | GC.BlkAttr.NO_INTERIOR;
D

当然のことながら、GCは、自身の関数によって予約されているメモリブロックのみ認識し、それらのメモリブロックのみをスキャンする。例えば、core.stdc.stdlib.callocによって割り当てられたメモリブロックについては認識しない。

GC.addRangeは、GCに無関係のメモリブロックを導入するためのものだ。補完関数GC.removeRangeは、core.stdc.stdlib.freeなど、他の手段でメモリブロックを解放する前に呼び出す必要がある。

場合によっては、メモリブロックがGCによって予約されていても、プログラム内にそのメモリブロックへの参照がないことがある。例えば、メモリブロックへの唯一の参照がCライブラリ内に存在する場合、GCは通常その参照を認識せず、そのメモリブロックは使用されなくなったものとみなす。

GC.addRootは、収集サイクル中にスキャンされるように、メモリブロックをルートとして GCに導入する。そのメモリブロックから直接または間接的に到達できるすべての変数は、存続中としてマークされる。メモリブロックが使用されなくなった場合は、補完関数GC.removeRootを呼び出す必要がある。

メモリ領域の拡張例

配列のように機能する単純なstructテンプレートを設計しよう。例を短くするため、要素の追加とアクセス機能のみを提供する。配列と同様に、必要に応じて容量を増やそう。次のプログラムは、上記で定義したreallocCleared()を使用している。

struct Array(T) {
    T * buffer;         // 要素を格納するメモリ領域
    size_t capacity;    // バッファの要素のキャパシティ
    size_t length;      // 実際の要素数

    /* 指定した要素を返す */
    T element(size_t index) {
        import std.string;
        enforce(index < length,
                format("Invalid index %s", index));

        return *(buffer + index);
    }

    /* 要素を末尾に追加する */
    void append(T element) {
        writefln("Appending element %s", length);

        if (length == capacity) {
            /* 新しい要素を格納するスペースがない; 容量を
             * 増やす必要がある。 */
            size_t newCapacity = capacity + (capacity / 2) + 1;
            increaseCapacity(newCapacity);
        }

        /* 要素を末尾に配置する */
        *(buffer + length) = element;
        ++length;
    }

    void increaseCapacity(size_t newCapacity) {
        writefln("Increasing capacity from %s to %s",
                 capacity, newCapacity);

        size_t oldBufferSize = capacity * T.sizeof;
        size_t newBufferSize = newCapacity * T.sizeof;

        /* また、このメモリブロックをポインタの検索対象から
         * 除外することも指定する。 */
        buffer = cast(T*)reallocCleared(
            buffer, oldBufferSize, newBufferSize,
          _SCAN);

        capacity = newCapacity;
    }
}
D

配列の容量は約50%増加する。例えば、100個の要素の容量が消費された後、新しい容量は151になる。(余分な1は、長さが0の場合、50%を追加しても配列が拡大しない場合のためだ。)

次のプログラムは、double型でそのテンプレートを使用している。

import std.stdio;
import core.memory;
import std.exception;

// ...

void main() {
    auto array = Array!double();

    const count = 10;

    foreach (i; 0 .. count) {
        double elementValue = i * 1.1;
        array.append(elementValue);
    }

    writeln("The elements:");

    foreach (i; 0 .. count) {
        write(array.element(i), ' ');
    }

    writeln();
}
D

出力:

インデックス0の要素を追加
容量を0から1に増やす
インデックス1の要素を追加
容量を1から2に増やす
インデックス2の要素を追加
容量を2から4に増やす
インデックス3の要素を追加
インデックス4の要素を追加
容量を4から7に増やす
インデックス5の要素を追加
インデックス6の要素を追加
インデックス7の要素を追加
容量を7から11に増やす
インデックス8の要素を追加
インデックス9の要素を追加
要素
01.12.23.34.45.56.67.78.89.9
アラインメント

デフォルトでは、すべてのオブジェクトは、そのオブジェクトの型に固有の量の倍数であるメモリ位置に配置される。その量は、その型の整列と呼ばれる。例えば、整数型intの整列は4だ。これは、整数型int変数は、4の倍数(4、8、12など)のメモリ位置に配置されるためだ。

アライメントは、CPUのパフォーマンスや要件のために必要だ。アライメントがずれたメモリアドレスにアクセスすると、アクセス速度が低下したり、バスエラーが発生したりする可能性があるからだ。さらに、特定の型の変数は、アライメントが正しいアドレスでのみ正しく動作する。

.alignofプロパティ

型の.alignofプロパティは、その型のデフォルトの整列値だ。クラスでは、.alignofはクラスオブジェクトではなく、クラス変数の整列だ。クラスオブジェクトの整列は、std.traits.classInstanceAlignmentで取得できる。

次のプログラムは、さまざまな型の配置を表示する。

import std.stdio;
import std.meta;
import std.traits;

struct EmptyStruct {
}

struct Struct {
    char c;
    double d;
}

class EmptyClass {
}

class Class {
    char c;
}

void main() {
    alias Types = AliasSeq!(char, short, int, long,
                            double, real,
                            string, int[int], int*,
                            EmptyStruct, Struct,
                            EmptyClass, Class);

    writeln(" Size  Alignment  Type\n",
            "=========================");

    foreach (Type; Types) {
        static if (is (Type == class)) {
            size_t size = __traits(classInstanceSize, Type);
            size_t alignment = classInstanceAlignment!Type;

        } else {
            size_t size = Type.sizeof;
            size_t alignment = Type.alignof;
        }

        writefln("%4s%8s      %s",
                 size, alignment, Type.stringof);
    }
}

プログラムの出力は環境によって異なる場合がある。以下の出力を参照しよう:

サイズアラインメント
11char
22short
44int
88long
88double
1616real
168string
88int[int]
88int*
11EmptyStruct
168Struct
168EmptyClass
178Class

後で、変数を特定のメモリ位置に構築(配置)する方法について説明する。正確性と効率のため、オブジェクトはアライメントと一致するアドレスに構築する必要がある。

上記のClass型の2つの連続したオブジェクト(それぞれ17バイト)を考えてみよう。0はほとんどのプラットフォームでは変数にとって有効なアドレスではないが、例を簡単にするために、最初のオブジェクトはアドレス0にあると仮定しよう。このオブジェクトの17バイトは、0から16までのアドレスにある。

     0    1           16
  ┌────┬────┬─ ... ─┬────┬─ ...
  │<────first object────>│
  └────┴────┴─ ... ─┴────┴─ ...

次に使用可能なアドレスは17だが、17はその型の整列値8の倍数ではないため、Classオブジェクトには使用できない。2番目のオブジェクトに最も近いアドレスは24である。24は8の次の最小の倍数だからだ。2番目のオブジェクトをそのアドレスに配置すると、2つのオブジェクトの間に未使用のバイトが発生する。これらのバイトはパディングバイトと呼ばれる。

     0    1           16   17           23   24   25           30
  ┌────┬────┬─ ... ─┬────┬────┬─ ... ─┬────┬────┬────┬─ ... ─┬────┬─ ...
  │<────first object────>│<────padding────>│<───second object────>│
  └────┴────┴─ ... ─┴────┴────┴─ ... ─┴────┴────┴────┴─ ... ─┴────┴─ ...

次の式で、オブジェクトを配置できる最も近いアドレス値を決定することができる。

(candidateAddress + alignmentValue - 1)
/ alignmentValue
* alignmentValue
D

この式を使用するには、除算の結果の小数部分は切り捨てられる必要がある。整数型では切り捨ては自動的に行われるため、上記の変数はすべて整数型であると仮定する。

以下の例では、次の関数を使用する。

T * nextAlignedAddress(T)(T * candidateAddr) {
    import std.traits;

    static if (is (T == class)) {
        const alignment = classInstanceAlignment!T;

    } else {
        const alignment = T.alignof;
    }

    const result = (cast(size_t)candidateAddr + alignment - 1)
                   / alignment * alignment;
    return cast(T*)result;
}
D

この関数テンプレートは、テンプレートパラメータからオブジェクトの型を推測する。型がvoid*の場合、これは不可能なので、void*のオーバーロードに明示的なテンプレート引数として型を指定する必要がある。このオーバーロードは、上記の関数テンプレートの呼び出しを簡単に転送できる。

void * nextAlignedAddress(T)(void * candidateAddr) {
    return nextAlignedAddress(cast(T*)candidateAddr);
}

上記の関数テンプレートは、emplace()によってクラスオブジェクトを構築する際に、以下で役立つ。

その型の2つのオブジェクトの間に配置しなければならないパディングバイトを含む、オブジェクトの合計サイズを計算する関数テンプレートをもう1つ定義しよう。

size_t sizeWithPadding(T)() {
    static if (is (T == class)) {
        const candidateAddr = __traits(classInstanceSize, T);

    } else {
        const candidateAddr = T.sizeof;
    }

    return cast(size_t)nextAlignedAddress(cast(T*)candidateAddr);
}
.offsetofプロパティ

アライメントは、ユーザー定義型のメンバーにも適用される。メンバーは、それぞれの型に応じてアライメントされるように、メンバー間にパディングバイトが挿入される場合がある。そのため、次のstructのサイズは、予想通り6バイトではなく12バイトになる。

struct A {
    byte b;     // 1バイト
    int i;      // 4バイト
    ubyte u;    // 1バイト
}

static assert(A.sizeof == 12);    // 1 + 4 + 1以上
D

これは、intメンバーが4の倍数となるアドレスにアラインメントされるように、その前にパディングバイトが挿入されているためだからだ。また、structオブジェクト全体のアラインメントのため、末尾にもパディングバイトが挿入されている。

.offsetofプロパティは、メンバー変数が、その変数が属するオブジェクトの先頭から何バイト目にあるかを示す。次の関数は、.offsetofによってパディングバイトを決定し、型のレイアウトを出力する。

void printObjectLayout(T)()
        if (is (T == struct) || is (T == union)) {
    import std.stdio;
    import std.string;

    writefln("=== Memory layout of '%s'" ~
             " (.sizeof: %s, .alignof: %s) ===",
             T.stringof, T.sizeof, T.alignof);

    /* レイアウト情報を1行で表示する。 */
    void printLine(size_t offset, string info) {
        writefln("%4s: %s", offset, info);
    }

    /* パディングが実際に確認された場合、
     * パディング情報を表示する。 */
    void maybePrintPaddingInfo(size_t expectedOffset,
                               size_t actualOffset) {
        if (expectedOffset < actualOffset) {
            /* 実際のオフセットが
             * 予想オフセットを超えているため、パディングがある。 */

            const paddingSize = actualOffset - expectedOffset;

            printLine(expectedOffset,
                      format("... %s-byte PADDING",
                             paddingSize));
        }
    }

    /* そのメンバーの前にパディングバイトがない場合の、
     * 次のメンバーの予想オフセット。 */
    size_t noPaddingOffset = 0;

    /* 注釈: __traits(allMembers)は、
     * 型のメンバー名の'文字列'コレクションだ。 */
    foreach (memberName; __traits(allMembers, T)) {
        mixin (format("alias member = %s.%s;",
                      T.stringof, memberName));

        const offset = member.offsetof;
        maybePrintPaddingInfo(noPaddingOffset, offset);

        const typeName = typeof(member).stringof;
        printLine(offset,
                  format("%s %s", typeName, memberName));

        noPaddingOffset = offset + member.sizeof;
    }

    maybePrintPaddingInfo(noPaddingOffset, T.sizeof);
}

次のプログラムは、上記で定義された12バイトのstruct Aのレイアウトを出力する:

struct A {
    byte b;
    int i;
    ubyte u;
}

void main() {
    printObjectLayout!A();
}

プログラムの出力は、オブジェクト内の6つのパディングバイトの合計の位置を示している。出力の最初の列は、オブジェクトの先頭からのオフセットだ:

'A'のメモリレイアウト (.sizeof: 12, .alignof: 4)
0byte b
1... 3バイトのパディング
4int i
8ubyte u
9... 3バイトのパディング

パディングを最小限に抑える1つの手法は、メンバーを大きいものから小さいものの順に並べ替えることだ。例えば、intメンバーを前のstructの先頭に移動すると、オブジェクトのサイズは小さくなる。

struct B {
    int i;    // 構造体定義内に移動
    byte b;
    ubyte u;
}

void main() {
    printObjectLayout!B();
}

この場合、オブジェクトのサイズは末尾の2バイトのパディングのみのため、8に減少する:

'B'のメモリレイアウト (.sizeof: 8, .alignof: 4)
0int i
4byte b
5ubyte u
6... 2バイトのパディング
align属性

align属性は、変数、ユーザー定義型、およびユーザー定義型のメンバーの位置合わせを指定するためのものだ。括弧内に指定した値は、位置合わせの値を指定する。すべての定義は個別に指定することができる。例えば、次の定義では、Sオブジェクトは2バイト境界に、そのiメンバーは1バイト境界に位置合わせされる(1バイトの位置合わせでは、パディングはまったく行われない)。

align (2)               // 'S'オブジェクトのアライメント
struct S {
    byte b;
    align (1) int i;    // メンバー'i'のアライメント
    ubyte u;
}

void main() {
    printObjectLayout!S();
}

intメンバーが1バイト境界にアラインメントされると、その前にパディングは存在せず、この場合オブジェクトのサイズは正確に6になる:

'S'のメモリレイアウト (.sizeof: 6, .alignof: 4)
0byte b
1int i
5ubyte u

alignはユーザー定義型のサイズを縮小できるが、型のデフォルトの整列が守られていない場合、パフォーマンスが大幅に低下する可能性がある(一部のCPUでは、整列されていないデータを使用すると、プログラムが実際にクラッシュする場合もある)。

align変数のアラインメントも指定できる:

align (32) double d;    // 変数のアライメント
D

ただし、newによって割り当てられたオブジェクトは、GCが想定しているように、size_t型のサイズの倍数に必ず整列する必要がある。そうしないと、未定義の動作になる。例えば、size_tの長さが8バイトの場合、newによって割り当てられた変数の整列は8の倍数でなければならない。

特定のメモリ位置に変数を構築する

new式は、3つのタスクを実行する。

  1. オブジェクトに十分な大きさのメモリを割り当てる。新しく割り当てられたメモリ領域は、型やオブジェクトに関連付けられていない生のメモリ領域と見なされる。
  2. その型に対応する.initの値をそのメモリ領域にコピーし、その領域でオブジェクトのコンストラクタを実行する。このステップが完了して初めて、オブジェクトはそのメモリ領域に配置される
  3. メモリブロックを構成し、オブジェクトが解放された際に適切に破棄されるための必要なフラグとインフラストラクチャをすべて設定する。

これらのタスクの最初のものは、GC.callocなどのメモリ割り当て関数によって明示的に実現できることはすでに説明した。Dはシステム言語であるため、プログラマは2番目のステップも管理することができる。

std.conv.emplaceを使用して、変数を特定の位置に構築することができる。

特定の場所に構造体オブジェクトを構築する

emplace()は、最初のパラメーターとしてメモリ位置のアドレスをとり、その位置にオブジェクトを構築する。パラメーターが指定された場合、残りのパラメーターをオブジェクトのコンストラクター引数として使用する:

import std.conv;
// ...
    emplace(address, /* ... コンストラクタの引数 ... */);
D

structオブジェクトを構築する場合、オブジェクトの型を明示的に指定する必要はない。emplace()は、ポインタの型からオブジェクトの型を推測するからだ。例えば、次のポインタの型はStudent*であるため、emplace()はそのアドレスにStudentオブジェクトを構築する。

	Student * objectAddr = nextAlignedAddress(candidateAddr);
// ...
    emplace(objectAddr, name, id);
D

次のプログラムは、3つのオブジェクトを格納するのに十分なメモリ領域を割り当て、そのメモリ領域内のアラインメントされたアドレスに1つずつオブジェクトを構築する。

import std.stdio;
import std.string;
import core.memory;
import std.conv;

// ...

struct Student {
    string name;
    int id;

    string toString() {
        return format("%s(%s)", name, id);
    }
}

void main() {
    /* この型に関する情報。 */
    writefln("Student.sizeof: %#x (%s) bytes",
             Student.sizeof, Student.sizeof);
    writefln("Student.alignof: %#x (%s) bytes",
             Student.alignof, Student.alignof);

    string[] names = [ "Amy", "Tim", "Joe" ];
    const totalSize = sizeWithPadding!Student() * names.length;

    /* すべてのStudentオブジェクト用の領域を確保する。
     *
     * 警告! このスライスからアクセスできるオブジェクトは
     * まだ構築されていない; 適切に構築されるまで、
     * これらのオブジェクトにアクセスしてはならない。 */
    Student[] students =
        (cast(Student*)GC.calloc(totalSize))[0 .. names.length];

    foreach (i, name; names) {
        Student * candidateAddr = students.ptr + i;
        Student * objectAddr =
            nextAlignedAddress(candidateAddr);
        writefln("address of object %s: %s", i, objectAddr);

        const id = 100 + i.to!int;
        emplace(objectAddr, name, id);
    }

    /* すべてのオブジェクトが構築され、使用可能になった。 */
    writeln(students);
}

プログラムの出力:

Student.sizeof0x18 (24)バイト
Student.alignof0x8 (8)バイト
オブジェクト0のアドレス7F1532861F00
オブジェクト1のアドレス7F1532861F18
オブジェクト2のアドレス7F1532861F30
[Amy(100), Tim(101), Joe(102)]
特定の位置にクラスオブジェクトを構築する

クラス変数は、クラスオブジェクトとまったく同じ型である必要はない。例えば、型Animalのクラス変数は、Catオブジェクトを参照することができる。このため、emplace()は、メモリポインタの型からオブジェクトの型を決定しない。その代わりに、オブジェクトの実際の型を、emplace()のテンプレート引数として明示的に指定する必要がある。(注釈:さらに、クラスポインタは、クラスオブジェクトではなく、クラス変数へのポインタだ。そのため、実際の型を指定することで、プログラマはクラスオブジェクトを配置するか、クラス変数を配置するかを指定することができる。)

クラスオブジェクトのメモリ位置は、次の構文でvoid[]スライスとして指定する必要がある。

Type variable =
    emplace!Type(voidSlice,
                     /* ... コンストラクタの引数 ... */);
D

emplace() 指定されたスライス位置にクラスオブジェクトを構築し、そのオブジェクトのクラス変数を返す。

Animal 階層オブジェクトに対してemplace()を使用しよう。この階層オブジェクトは、GC.callocによって割り当てられたメモリ領域に並べて配置される。例をもっと面白くするために、サブクラスのサイズを異なるものにする。これは、前のオブジェクトのサイズに応じて、次のオブジェクトのアドレスを決定する方法を示すのに役立つ。

interface Animal {
    string sing();
}

class Cat : Animal {
    string sing() {
        return "meow";
    }
}

class Parrot : Animal {
    string[] lyrics;

    this(string[] lyrics) {
        this.lyrics = lyrics;
    }

    string sing() {
        /* std.algorithm.joinerは、指定された区切り文字で
         * 範囲の要素を結合する。 */
        return lyrics.joiner(", ").to!string;
    }
}

オブジェクトを格納するバッファは、GC.callocで割り当てられる。

const capacity = 10_000;
void * buffer = GC.calloc(capacity);
D

通常、オブジェクト用の容量が常に確保されていることを確認する必要がある。ここでは、例を単純にするためにそのチェックは無視し、例のオブジェクトは 1 万バイトに収まるものと仮定する。

バッファは、CatParrotオブジェクトの構築に使用される:

Cat cat = emplace!Cat(catPlace);
// ...
Parrot parrot =
    emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);
D

Parrotのコンストラクタ引数は、オブジェクトのアドレスの後に指定されることに注意。

emplace()が返す変数は、後でforeachループで使用するためにAnimalスライスに格納される:

Animal[] animals;
// ...
animals ~= cat;
// ...
animals ~= parrot;

foreach (animal; animals) {
    writeln(animal.sing());
}
D

詳細な説明はコードのコメント内に記載されている。

import std.stdio;
import std.algorithm;
import std.conv;
import core.memory;

// ...

void main() {
    /* Animal変数(Animalオブジェクトではない)のスライス。 */
    Animal[] animals;

    /* 任意の容量のバッファを割り当て、
     * この例では2つのオブジェクトがその領域に収まると仮定する。
     * 通常、この条件は
     * 検証する必要がある。 */
    const capacity = 10_000;
    void * buffer = GC.calloc(capacity);

    /* まず、Catオブジェクトを配置しよう。 */
    void * catCandidateAddr = buffer;
    void * catAddr = nextAlignedAddress!Cat(catCandidateAddr);
    writeln("Cat address   : ", catAddr);

    /* emplace()はクラスオブジェクトに対してvoid[]を必要とするため、
     * まずポインタからスライスを生成する必要がある。 */
    size_t catSize = __traits(classInstanceSize, Cat);
    void[] catPlace = catAddr[0..catSize];

    /* そのメモリスライス内にCatオブジェクトを構築し、
     * 返されたクラス変数を後で使用するために格納する。 */
    Cat cat = emplace!Cat(catPlace);
    animals ~= cat;

    /* 次に、アライメント要件を満たす次の利用可能なアドレスに
     * Parrotオブジェクトを構築する。 */
    void * parrotCandidateAddr = catAddr + catSize;
    void * parrotAddr =
        nextAlignedAddress!Parrot(parrotCandidateAddr);
    writeln("Parrot address: ", parrotAddr);

    size_t parrotSize = __traits(classInstanceSize, Parrot);
    void[] parrotPlace = parrotAddr[0..parrotSize];

    Parrot parrot =
        emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);
    animals ~= parrot;

    /* オブジェクトを使用する。 */
    foreach (animal; animals) {
        writeln(animal.sing());
    }
}

出力:

Catのアドレス7F0E343A2000
Parrotのアドレス7F0E343A2018
meow
squawk, arrgh

main()内の手順をオブジェクトごとに繰り返す代わりに、newObject(T)のような関数テンプレートを使用するとより便利だ。

オブジェクトの明示的な破棄

new演算子の逆操作は、オブジェクトを破棄し、オブジェクトのメモリをGCに返すことだ。通常、これらの操作は指定されていないタイミングで自動的に実行される。

しかし、プログラム内の特定の時点でデストラクタを実行する必要がある場合もある。例えば、オブジェクトがデストラクタ内でFileメンバーを閉じている場合、そのオブジェクトのライフタイムが終了したらすぐにデストラクタを実行しなければならない。

destroy()オブジェクトのデストラクタを呼び出す:

destroy(variable);
D

デストラクタの実行後、destroy()は変数をその.init状態に設定する。クラス変数の.init状態はnullであるため、クラス変数は一度破棄されると使用できなくなることに注意しよう。destroy()は、デストラクタを実行するだけである。破棄されたオブジェクトが占めていたメモリをいつ再利用するかは、GCに依存する。

警告: 構造体ポインタで使用する場合、destroy()はポインタではなく、ポインタが指すオブジェクトを受け取る必要がある。そうしないと、ポインタはnullに設定されるが、オブジェクトは破棄されない。

import std.stdio;

struct S {
    int i;

    this(int i) {
        this.i = i;
        writefln("Constructing object with value %s", i);
    }

    ~this() {
        writefln("Destroying object with value %s", i);
    }
}

void main() {
    auto p = new S(42);

    writeln("Before destroy()");
    destroy(p);                        // ← 誤った使用法
    writeln("After destroy()");

    writefln("p: %s", p);

    writeln("Leaving main");
}

destroy()がポインタを受け取った場合、破壊されるのはポインタそのもの(つまり、ポインタがnullになる):

値42でオブジェクトを構築中
destroy()の前
destroy()の後  ← この行の前にオブジェクトが破壊されていない
p: null          ←  代わりにポインタがnullになる
mainを離れる
値42のオブジェクトを破壊中

そのため、構造体ポインタで使用する場合、destroy()はポインタではなく、ポインタが指すオブジェクトを受け取る必要がある。

destroy(*p);                       // ← 正しい使い方
D

この場合、デストラクタは正しい場所で実行され、ポインタはnullに設定されない:

値42のオブジェクトを構築中
destroy()の前
値42のオブジェクトを破壊中  ← 正しい位置で破壊された
destroy()の後
p: 7FB64FE3F200                  ← ポインタはnullではない
mainを離れる
値0のオブジェクトを破棄中   ← S.initのために再度

最後の行は、同じオブジェクトに対してデストラクタがもう一度実行されたため。このオブジェクトの値は、S.initになっている。

実行時に名前でオブジェクトを構築する

Objectのメンバ関数factory()は、クラス型の完全修飾名をパラメータとして受け取り、その型のオブジェクトを構築し、そのオブジェクトのクラス変数を返す。

module pind.samples.ja.memory.memory_10;

import std.stdio;

interface Animal {
    string sing();
}

class Cat : Animal {
    string sing() {
        return "meow";
    }
}

class Dog : Animal {
    string sing() {
        return "woof";
    }
}

void main() {
    string[] toConstruct = [ "Cat", "Dog", "Cat" ];

    Animal[] animals;

    foreach (typeName; toConstruct) {
        /* 擬似変数__MODULE__は常に現在のモジュールの名前で、
         * コンパイル時に文字列リテラルとして使用できる。
         */
        const fullName = __MODULE__ ~ '.' ~ typeName;
        writefln("Constructing %s", fullName);
        animals ~= cast(Animal)Object.factory(fullName);
    }

    foreach (animal; animals) {
        writeln(animal.sing());
    }
}

このプログラムには明示的なnew式は含まれていないが、3つのクラスオブジェクトが作成され、animalsスライスに追加されている。

pind.samples.ja.memory.memory_10.Catを構築中
pind.samples.ja.memory.memory_10.Dogを構築中
pind.samples.ja.memory.memory_10.Catを構築中
ニャー
ワン
ニャー

Object.factory()は、オブジェクトの型の完全修飾名を受け取ることに注意。また、factory()の戻り値の型はObjectなので、プログラムで使用する前に、オブジェクトの実際の型にキャストする必要がある。

要約