Scala Native の Immix GC 実装の使い方
Dec 15, 2018 00:00 · 2463 words · 5 minute read
※この記事は 言語実装 Advent Calendar 2018の15日目の記事です。
はじめに
ブログがあったことすら完全に忘れていました。
Immix1は Blackburn らが提案した高速なガベージコレクション(GC)の手法です。 Scala Nativeではv0.3.0においてこのImmix GCを実装し、従来のBohem GCに代えて使用できるようになりました。 また、Scala NativeではImmix GCの実装を言語処理系自身から分離したものを公開しています。
このImmix GC 実装は他の言語処理系からの利用が容易になるよう配慮されている(と個人的に思っている)のですが、どこを探してもまともな資料がありません。そこで本記事では私が調べた、この実装(以降はlibImmixと呼ぶことにします)を自作処理系に組み込むための基本的な知見等について書き残し、供養しておきたいと思います2。
ユーザとして使用する分には、Immix GCはマークアンドスウィープGCとほぼ同じように扱える3ため、マークアンドスウィープの概要を頭の片隅に置きながら、以後の文章を読んでいただければと思います。
GitHubの該当リポジトリからclone
し、readmeにあるとおりにビルドしてください4。build/
にlibImmix.a
ができるので、これを使います。
libImmixの使い方
libImmixと共有するグローバル変数
libImmixを使用するにあたり、以下の3つのグローバル変数をlibImmix側から見られるようにしておく必要があります。
uintptr_t *__modules;
int __modules_size;
uintptr_t **__stack_bottom;
これらはガベージコレクタが marking を行う際のルートとして使用されます。
__modules
および__modules_size
はstaticな寿命を持つオブジェクト5の配列であり、後述するscalanative_alloc
系の関数の返値の配列を設定します。
また、__stack_bottom
はスタックの底へのポインタであり、ガベージコレクタはスタックの先頭からこの変数に設定したアドレスまでをGCの対象とします。
ガベージコレクタはこの2つの領域(__modules
を先頭とする要素数__modules_size
の配列および、スタックの頭から__stack_bottom
まで)を走査し、
その中に自身が確保したメモリ領域へのポインタを探し出し、オブジェクトをたどる起点とします。
libImmixが提供する関数
libImmixは以下の関数を提供します。
extern "C" void scalanative_init();
extern "C" void *scalanative_alloc(void *info, size_t size, int isObjectArray);
extern "C" void *scalanative_alloc_small(void *info, size_t size);
extern "C" void *scalanative_alloc_large(void *info, size_t size);
extern "C" void scalanative_collect();
scalanative_init
とscalanative_collect
はその名の通り、ガベージコレクタの初期化と明示的なオブジェクト回収を行います。
scalanative_alloc
系の関数は、size
分のメモリをガベージコレクタが管理する領域から確保します。また、info
には後述のRtti
構造体へのポインタをvoid*
にして渡します。
alloc
系の関数の返値は、以下のような構造体へのポインタにキャストして使うことができます。
struct Obj {
Rtti* info;
uintptr_t body[0];
};
このうち、info
にはalloc
系関数の引数に渡した値がそのままセットされます。また、body
が、ユーザが使用できる領域の先頭になります。
body
にはalloc
系関数の引数size
の大きさの領域が確保されます。
scalanative_alloc
関数では、isObjectArray
をtrue
にすると、その領域をオブジェクトの配列と見なし、marking時にその領域すべてを走査するようになります。
scalanative_alloc_small
とscalanative_alloc_large
の使い分けですが、Scala Nativeでは、8192 bytes 以上のオブジェクトに対してlarge
を、それ未満のオブジェクトに対してsmall
を使用しているようです67。
Rtti構造体
Rtti
構造体はその名の通り、Scalaの実行時型情報(RTTI)を扱う構造体のようです。
Rtti
構造体は以下のように定義されています(ObjectHeader.hより)。
typedef struct {
struct {
int32_t id;
word_t *name;
int8_t kind;
} rt;
int64_t size;
struct {
int32_t from;
int32_t to;
} range;
struct {
int32_t dyn_method_count;
word_t *dyn_method_salt;
word_t *dyn_method_keys;
word_t *dyn_methods;
} dynDispatchTable;
int64_t *refMapStruct;
} Rtti;
この内、GCに必要となるのはrefMapStruct
のみです。実際にlibImmixを自作言語処理系に組み込む場合は、ObjectHeader.h
の各種構造体を
自身の処理系で使っているものと同じになるように書き換えた方が良いかもしれません。
refMapStruct
は、その型のどこに他のオブジェクトへの参照を持っているかを表す配列です。
上記Obj
構造体のbody
フィールドのうち、他のオブジェクトのアドレスとなっている要素のインデックスを並べ、最後に番兵として-1
を置きます。
その他
Log.h
内の#define DEBUG_PRINT
を有効にすると、GCが走ったタイミングでどれくらいの領域が開放されたかなどの情報が表示されるようになります。
以上が私が調べた範囲でのlibImmixの基本的な使い方です。 libImmixはScala Licenseで提供されているため、ライセンス的に自作言語処理系に組み込みやすく、何よりBoehmGCに比べてかなり高速なのが嬉しいところですね。
慌てて書いたので誤り等多々あるかと思います。お気づきの際は優しくお知らせいただければと思います。
- http://users.cecs.anu.edu.au/~steveb/pubs/papers/immix-pldi-2008.pdf [return]
- 私が作成している言語処理系は、GCを組み込む段階まで到達するのがかなり先になりそうなので…… [return]
- 上記の元論文では、”This paper describes a collector family, called mark-region, and introduces opportunistic defragmentation, which mixes copying and marking in a single pass. Combining both, we implement immix,…” らしいですが [return]
- ビルドにはlibunwindが必要です。また、手元の環境(Arch Linux on WSL, GCC)では、GCTypes.hのインライン展開周りに手を入れないとビルドが通らなかった覚えがあります [return]
- ここでは、自作言語処理系が生成し、libImmixを使用してメモリ管理を行うオブジェクトを単にオブジェクトと呼ぶことにします。 [return]
- https://github.com/scala-native/scala-native/blob/04525ae624ade17e4e7bc6a8b5695903762f40e4/tools/src/main/scala/scala/scalanative/codegen/Lower.scala#L823 [return]
- https://github.com/scala-native/scala-native/blob/04525ae624ade17e4e7bc6a8b5695903762f40e4/tools/src/main/scala/scala/scalanative/codegen/Lower.scala#L481 [return]