開発日記

JITコンパイラの開発日記だった

トレース選択器の起動

Ruby向けJITコンパイラ RuJIT Advent Calendar 7日目です.

そろそろRuJITの中身についても書くことがなくなってきましたが,今日もRuJITの中身について書きます.今日はRuJITを起動するエントリーポイントの部分を見ていきます.

RuJITを起動するためのフック関数

RuJITを起動するためのフック関数はトレース記録用に2つ,トレース無効化に1つ用意されており,それぞれ以下のインターフェースで定義されています.(method-JIT用にもう1つ用意されていますが今回は省略します.)なお,今日はトレース記録用APIについて,明日はトレース無効化APIについて注目して書こうと思います.

void rujit_record_insn(rb_thread_t *th, rb_control_frame_t *reg_cfp, VALUE *reg_pc);
int rujit_invoke_or_make_trace(rb_thread_t *th, rb_control_frame_t *reg_cfp, VALUE *reg_pc);
void rujit_invalidate(VALUE obj);

上記2つのトレース記録用APIVMの実装内部に埋め込まれており,分岐命令(branchif, jump, branchunless)の実行やトレース記録命令(rectrace)にそれぞれ埋め込まれています.

具体的なフック関数の呼出は以下のコードで実行されます.

// insns.def
1181 /**
1182 @c jump
1183 @e if val is not false or nil, set PC to (PC + dst).
1184 @j もし val が false か nil でなければ、PC を (PC + dst) にする。
1185 */
1186 DEFINE_INSN
1187 branchif
1188 (OFFSET dst)
1189 (VALUE val)
1190 ()
1191 {
1192 if (RTEST(val)) {
1193 VALUE *pc = GET_PC();
1194 RUBY_VM_CHECK_INTS(th);
1195 JUMP(dst);
1196 if (dst < 0) {
1197 int invoked = rujit_invoke_or_make_trace(th, GET_CFP(), pc, BIN(branchif));
1198 if (invoked) {
1199 RESTORE_REGS();
1200 SET_PC(REG_CFP->pc);
1201 TC_DISPATCH_ORIGN(__JIT__);
1202 }
1203 }
1204 }
1205 }

// insns.def
2238 /**
2239 @c optimize
2240 @e invoke rujit compiler for recording hot path
2241 @j RuJITコンパイラを起動
2242 */
2243 DEFINE_INSN
2244 rectrace
2245 (VALUE dummy)
2246 ()
2247 ()
2248 {
2249 SET_PC(GET_PC() - 2);
2250 rujit_record_insn(th, GET_CFP(), GET_PC());
2251 RESTORE_REGS();
2252 TC_DISPATCH_ORIGN(rectrace);
2253 }

分岐命令の場合はフック関数は前方向へのジャンプの場合に実行され,rectrace命令では常にフック関数が実行されていることがわかりました.

この2つのAPIが具体的にどのような処理を行っているかについて見ていきます.
1つめのAPI,rujit_invoke_or_make_traceはコンパイル済みトレースを呼び出すもしくはトレースを新たに構築するAPIとなっています.
いくつかのコードを省略していますが,具体的なコードは以下のとおりです.

 

66 int rujit_invoke_or_make_trace(rb_thread_t *th, rb_control_frame_t *reg_cfp, VALUE *reg_pc)
67 {
68 jit_event_t ebuf, *e;
69 rujit_t *jit = current_jit;
70 jit_trace_t *trace;
78 trace = find_trace(jit, e);
79
80 if (trace_is_compiled(trace)) {
81 return trace_invoke(jit, e, trace);
82 }

コードキャッシュにコンパイル済みトレースがすでにあった場合には,トレースを実行します.

83 if (is_backward_branch(e)) {
84 if (trace == NULL) {
85 trace = rujit_alloc_trace(jit, e, NULL);
86 }
87 trace->start_pc = reg_pc;
88 }

トレースがまだ作られていない場合には空のトレースを生成します.

90 if (trace) {
91 trace->counter += 1;
92 if (trace->counter > HOT_TRACE_THRESHOLD) {
94 if (find_trace_in_blacklist(jit, trace)) {
95 return 0;
96 }
97 start_recording(jit, trace);
102 }
103 }
104 return 0;
105 }

最後にトレースの実行回数がしきい値を超えた場合にはトレースの記録を開始するという仕組みです.とくに難しいコードはありません.

ここで,start_recording関数,stop_recording関数は変数rujit_record_trace_modeのフラグに1を立てたり消したりする関数です.
rujit_record_trace_modeは後々使う変数なので頭の隅においておきます.

 

258 static void start_recording(rujit_t *jit, trace_t *trace)
259 {
260 rujit_record_trace_mode = 1;
265 }

267 static void stop_recording(rujit_t *jit)
268 {
269 rujit_record_trace_mode = 0;
274 }

もう1つのAPIであるrujit_record_insnも見ておきます.

 

48 void rujit_record_insn(rb_thread_t *th, rb_control_frame_t *reg_cfp, VALUE *reg_pc)
49 {
50 rujit_t *jit = current_jit;
55 assert(is_recording(jit));
56 e = jit_event_init(&ebuf, jit, th, reg_cfp, reg_pc);
57 if (is_end_of_trace(recorder, e)) {
58 rujit_push_compile_queue(jit, recorder->cur_func);
59 stop_recording(jit);
60 }
61 else {
62 record_insn(recorder, e);
63 }
64 }

この関数では,トレース記録の終了条件を確認し,トレース記録がまだ終わっていない場合はコードを記録していきます.

さて,rujit_invoke_or_make_traceは既存のYARV命令の実装に呼出コードが追加されているため,どのように呼び出されるか明白ですが,もう1つのAPI,rujit_record_insnはYARVに新たに追加した命令rectraceの内部で呼び出されています.この命令がどのような場合に呼び出されるかについて見ていきます.

rectrace命令はバイトコードコンパイラには生成用コードはありません.
コンパイラ支援なしにどのようにこの命令を実行するかの工夫はVM実行部分にあります.

VMの評価部vm_exec_core()内部ではこのようなマクロが用意されており,このマクロは各バイトコード命令の末尾で呼び出されます.

#define NEXT_INSN() TC_DISPATCH(__NEXT_INSN__)

ところでこのNEXT_INSNですが,具体的な実装は以下のTC_DISPATCHマクロにて実装されています.

 

102 #define TC_DISPATCH(insn) do {\
103 void const *next_addr = (rujit_record_trace_mode) ? LABEL_PTR(rectrace) : (void const *)GET_CURRENT_INSN();\
104 goto *next_addr; \
105 } while (0);

YARVはdirect threaded codeで実装されていますのでバイトコード上に埋め込まれたアドレスにジャンプする実装となっていますが,トレース記録中(rujit_record_trace_mode = 1)は必ずrectrace命令部分に飛ぶようになってます.これで新しいバイトコード命令(rectrace)は追加したものの,比較的小さなオーバヘッドでトレース記録のon/offを行っています.

 

まとめ

本稿ではRuJITのトレース記録用APIについて述べた.またVMに埋め込まれている起動のための工夫について述べ,どのように呼び出されるかについても解説した.