Julia オブジェクトのメモリレイアウト

オブジェクトレイアウト (jl_value_t)

jl_value_t 構造体は Julia のガベージコレクタが保有するメモリブロックに対する名前であり、Julia オブジェクトに関連するメモリ上のデータを表現します。この構造体に型情報は含まれず、実体は不透明なポインタです1:

typedef struct _jl_value_t jl_value_t;

それぞれの jl_value_t 構造体には、ガベージコレクタの到達性や型といった Julia オブジェクトに関するメタデータを持つ jl_taggedvalue_t 構造体2含まれます:

typedef struct {
    <不透明メタデータ>
    jl_value_t value;
} jl_taggedvalue_t;

Julia オブジェクトの型は jl_datatype_t オブジェクトが表す葉型のインスタンスです。このインスタンスは jl_typeof() で取得できます:

jl_value_t *jl_typeof(jl_value_t *v);

オブジェクトのレイアウトは型によって異なります。レイアウトを調べるにはリフレクションメソッドを使い、フィールドにアクセスするには get メソッドを使います:

jl_value_t *jl_get_nth_field_checked(jl_value_t *v, size_t i);
jl_value_t *jl_get_field(jl_value_t *o, char *fld);

フィールドの型が全てポインタだと事前に分かっているなら、配列としてアクセスすることで値を直接取得できます:

jl_value_t *v = value->fieldptr[n];

例えば「ボックス化」された uint16_t は次のように格納されます:

struct {
    <不透明メタデータ>
    struct {
        uint16_t data;        // -- 二バイト
    } jl_value_t;
};

このオブジェクトは jl_box_uint16() で作成されます。jl_value_t 型の値を指すポインタが構造体データの途中を指すことに注意してください。構造体の先頭にあるメタデータではありません。

値は様々な状況で「ボックス化解除」される (メタデータを省略してデータだけが、ときにはレジスタを通して、使われる) ので、ボックスのアドレスを値の一意な識別子とみなしてはいけません。未知のオブジェクトの等価比較には Julia の === に対応する "精密egal" なテストを使うべきです:

int jl_egal(jl_value_t *a, jl_value_t *b);

jl_valut_t ポインタが必要なときオブジェクトはオンデマンドに「ボックス化」されるので、この最適化による API への影響は比較的軽微です。

jl_value_t ポインタが指すメモリ上のオブジェクトの改変は、オブジェクトが可変なときに限って許されます。そうでないオブジェクトを書き換えるとプログラムが破壊される可能性があり、結果は未定義となります。値が可変かどうかは次の関数で取得できます:

int jl_is_mutable(jl_value_t *v);

格納されようとしているオブジェクトが jl_value_t なら、Julia のガベージコレクタにそのことを通知する必要があります:

void jl_gc_wb(jl_value_t *parent, jl_value_t *ptr);

様々な型の値に対するボックス化とボックス化解除、およびガベージコレクタとの対話についてはマニュアル Julia の組み込みの章にさらに説明があります。

Julia の組み込み型に対応する C 構造体は julia.h で定義されます。また Julia の組み込み型に対応する jl_datatype_t 型のグローバルオブジェクトは jltypes.cjl_init_types で初期化されます。

ガベージコレクタのマークビット

ガベージコレクタは jl_taggedvalue_t のメタデータ部分から 16 ビットを利用してシステム内のオブジェクトを追跡します。このアルゴリズムの詳細は gc.c にある実装に付いているコメントにあります。

オブジェクトのアロケート

ほとんどの新しいオブジェクトは jl_new_structv() でアロケートされます:

jl_value_t *jl_new_struct(jl_datatype_t *type, ...);
jl_value_t *jl_new_structv(jl_datatype_t *type, jl_value_t **args, uint32_t na);

ただし isbits なオブジェクトはメモリから直接構築することもできます:

jl_value_t *jl_new_bits(jl_value_t *bt, void *data)

また一部のオブジェクトではこれらの関数とは異なる特別なコンストラクタが利用できます:

これらの関数の他にも特殊なアロケート関数が存在しますが、ここには通常の用途で使われる関数だけを示しています。完全なリストはヘッダーファイル julia.h を参照してください。

Julia 内部でメモリをアロケートするときは通常 jl_gc_alloc で行われます3:

jl_value_t *jl_gc_alloc(jl_ptls_t ptls, size_t sz, void *ty);

jl_gc_alloc ではガベージコレクタがメモリをアロケートし、その後 jl_set_typeof が型のタグを付けます:

void jl_set_typeof(jl_value_t *v, jl_datatype_t *type);

全てのオブジェクトは四の倍数のバイト数でアロケートされ、プラットフォームのポインタサイズにアラインされることに注意してください。小さなオブジェクトではプールからメモリがアロケートされ、大きいオブジェクトでは malloc() が直接使われます。

シングルトン型

シングルトン型のインスタンスは一つだけであり、データフィールドは存在しません。シングルトンインスタンスのサイズは 0 バイトであり、メタデータだけからなります。例えば Nothing はシングルトン型で、nothing はその唯一のインスタンスです。

詳しくはマニュアルのシングルトン型および無値と欠損値の節を参照してください。


  1. 訳注: 現在のバージョンの定義に変更した。なお struct _jl_value_t は定義されない。[return]

  2. 訳注: jl_typetag_tjl_taggedvalue_t に改名されたので名前を変更した。[return]

  3. 訳注: 英語版には「メモリのアロケートには通常 newstruct() または newobj() が使われる」とあるが、現在のバージョンにこれらの関数が存在しないので変更した (参照: #17116)。[return]

広告