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 の ===
に対応する "精密" なテストを使うべきです:
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.c
の jl_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)
また一部のオブジェクトではこれらの関数とは異なる特別なコンストラクタが利用できます:
-
型:
jl_datatype_t *jl_apply_type(jl_datatype_t *tc, jl_tuple_t *params); jl_datatype_t *jl_apply_array_type(jl_datatype_t *type, size_t dim);
通常使われるのはこの二つですが、さらに低水準なコンストラクタも存在します。
julia.h
にある宣言から確認できます。これらの低水準なコンストラクタはjl_init_types()
で Julia システムイメージの作成をブートストラップするとき最初に必要となる型を作成するときに使われます。 -
タプル:
jl_tuple_t *jl_tuple(size_t n, ...); jl_tuple_t *jl_tuplev(size_t n, jl_value_t **v); jl_tuple_t *jl_alloc_tuple(size_t n);
Julia のオブジェクト表現エコシステムにおいてタプルは非常に特別な扱いを受けます。タプルオブジェクトが内部オブジェクトを指すポインタの配列となり、次の構造体と等価になる場合もあります:
typedef struct { size_t length; jl_value_t *data[length]; } jl_tuple_t;
しかしときには、タプルが無名の
isbits
型に変換されてボックス化されずに格納される、あるいは (jl_value_t*
として一般的な文脈で使われないために) そもそも格納されない場合もあります。 -
シンボル:
jl_sym_t *jl_symbol(const char *str);
-
関数と
MethodInstance
:jl_function_t *jl_new_generic_function(jl_sym_t *name); jl_method_instance_t *jl_new_method_instance(jl_value_t *ast, jl_tuple_t *sparams);
-
配列:
jl_array_t *jl_new_array(jl_value_t *atype, jl_tuple_t *dims); jl_array_t *jl_new_arrayv(jl_value_t *atype, ...); jl_array_t *jl_alloc_array_1d(jl_value_t *atype, size_t nr); jl_array_t *jl_alloc_array_2d(jl_value_t *atype, size_t nr, size_t nc); jl_array_t *jl_alloc_array_3d(jl_value_t *atype, size_t nr, size_t nc, size_t z); jl_array_t *jl_alloc_vec_any(size_t n);
これらの関数の他にも特殊なアロケート関数が存在しますが、ここには通常の用途で使われる関数だけを示しています。完全なリストはヘッダーファイル 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()
が直接使われます。