Feature #18875 https://bugs.ruby-lang.org/issues/18875 で、Ruby になにやら大きな変更が入ったようです。 チケットを読んでもどういうものなのかまだあまりわからず、気になったので調べつつメモします。
もっとも特徴的なのはこの部分です。
@@ -456,9 +460,12 @@ struct rb_iseq_constant_body {
} variable;
unsigned int local_table_size;
- unsigned int is_size;
+ unsigned int ic_size; // Number if IC caches
+ unsigned int ise_size; // Number of ISE caches
+ unsigned int ivc_size; // Number of IVC and ICVARC caches
unsigned int ci_size;
unsigned int stack_max; /* for stack overflow check */
+ iseq_bits_t * mark_offset_bits; /* Find references for GC */
char catch_except_p; /* If a frame of this ISeq may catch exception, set TRUE */
// If true, this ISeq is leaf *and* backtraces are not used, for example,
rb_iseq_constant_body 構造体から is_size という要素を削除し、ic_size, ise_size, ivc_size という要素を追加しています。さらに mark_offset_bits という要素も追加しています。
まずこの、削除された is_size とは何でしょうか。 古くは 30b1947df2da2192f7fcc812ac88dc1884715322 = r42637 で導入されたもののようです。
r42637 は ruby-trunk-changes によると
正規表現の o オプション
/#{expr}/o
は(略)大域脱出が発生すると評価中の状態のままになって再度評価しようとするとデッドロックするという不具合があり、VM の命令に once を導入して修正しています。かわりに onceinlinecache 命令を削除しています。
とのことでした。またコミットログによると
vm_core.h: `union iseq_inline_storage_entry' to store once data.
とのことでした。もともとは inline cache つまり高速化のためのキャッシュだけだったのが、動作のために必要なストレージとして使われるようになったというもののようです。 iseq 用の作業領域と考えてよさそうです。
さてそこから考えると、汎用的に用意された is なるストレージ が IC caches, ISE caches, IVC and ICVARC caches という明確な役割を与えられたということのようです。
では IC, ISE, IVC, ICVARC がなにかを見てみます。
case TS_IC: /* inline cache: constants */
...
case TS_ISE: /* inline storage entry: `once` insn */
...
case TS_ICVARC: /* inline cvar cache */
case TS_IVC: /* inline ivar cache */
定数のキャッシュ、一度だけ有効な once のためのストレージ、クラス変数・インスタンス変数のキャッシュということのようです。 この変更までは、once の処理のためと inline cache のための複数の用途が入り混じってかつその個数も一緒くたになっていたところを、inline cache を細分化したこの期に用途別に個数を管理するようになった、ということのようです。 このように inline cache を細分化することでどう嬉しいかは後述します。
次に特徴的なのはこの部分です。
+#define ISEQ_MBITS_SIZE sizeof(iseq_bits_t)
+#define ISEQ_MBITS_BITLENGTH (ISEQ_MBITS_SIZE * CHAR_BIT)
+#define ISEQ_MBITS_SET(buf, i) (buf[(i) / ISEQ_MBITS_BITLENGTH] |= ((iseq_bits_t)1 << ((i) % ISEQ_MBITS_BITLENGTH)))
+#define ISEQ_MBITS_SET_P(buf, i) ((buf[(i) / ISEQ_MBITS_BITLENGTH] >> ((i) % ISEQ_MBITS_BITLENGTH)) & 0x1)
+#define ISEQ_MBITS_BUFLEN(size) (((size + (ISEQ_MBITS_BITLENGTH - 1)) & -ISEQ_MBITS_BITLENGTH) / ISEQ_MBITS_BITLENGTH)
+
ビットマップでなにかの有無を管理するためのマクロのようです。
ISEQ_MBITS_SET を追いかけると、どのようなときにこのビットマップがマークされるのかわかります。
どうやら iseq_set_sequence()
や ibf_load_code()
のように iseq を新しく作り出す箇所で generated_iseq[index] = v
のように代入され
かつ RB_OBJ_WRITTEN(iseq, Qundef, v)
や FL_SET(iseqv, ISEQ_MARKABLE_ISEQ)
が必要になるとき、つまりマークするべき Ruby オブジェクトが iseq に入ったときにその位置を記録する、ということのようです。
さて、前半の inline cache の管理細分化と、後半の iseq の Ruby オブジェクト位置の管理、両者はそれぞれあまり関係がないように見えますが、これがなぜ必要になったのかというのが rb_iseq_each_value()
の部分です。
iseq.c の中で、以下の部分が消えています。
@@ -317,19 +318,69 @@ rb_iseq_each_value(const rb_iseq_t *iseq, iseq_value_itr_t * func, void *data)
{
unsigned int size;
VALUE *code;
- size_t n;
- rb_vm_insns_translator_t *const translator =
-#if OPT_DIRECT_THREADED_CODE || OPT_CALL_THREADED_CODE
- (FL_TEST((VALUE)iseq, ISEQ_TRANSLATED)) ? rb_vm_insn_addr2insn2 :
-#endif
- rb_vm_insn_null_translator;
const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq);
size = body->iseq_size;
code = body->iseq_encoded;
- for (n = 0; n < size;) {
- n += iseq_extract_values(code, n, func, data, translator);
rb_iseq_each_value()
というのは、rb_iseq_mark()
や rb_iseq_update_references()
で呼び出され、iseq の中に埋まっている GC 管理対象の Ruby オブジェクトに対してなにか処理をする関数です。
iseq_extract_values()
というのは、iseq から types を取り出して type ごとの処理で iseq に埋め込まれた VALUE つまり Ruby オブジェクトに対して第 3 引数の func() を順次呼び出していく、という関数です。iseq を解釈し逆コンパイルのようなことをして埋め込まれた値を取り出すと考えて良いと思います。
つまり、今までは iseq の中身を解釈してやらなければ内部に埋め込まれた GC 対象の Ruby オブジェクトが取り出せなかったところを、iseq 中の管理の方法を変えることで Ruby オブジェクトを簡単に取り出せるようになった、そのため GC でのマークやリファレンス更新などの処理が高速化した、ということのようでした。
と、ここまで調べてから冒頭の 18875 に戻ると、なるほどほとんどはチケットにしっかり書かれていました。というよりは書かれている内容がなんとなくわかるようになっていました。なるほどよくできている。