insns.def周りの理解を整理するために書いた雑文を置いておく。ソースのバージョンは2.4.1
YARVの命令ディスパッチを行う関数: vm_exec_core
vm_exec.c に vm_exec_core なる関数がある。perf recordでRubyのスクリプトを実行してperf reportするとかなり大部分をvm_exec_coreが消費していることが多い。
これはRubyのコードがYARV iseqにコンパイルされた後、この関数がYARV iseqのインタプリタ的に働くことでCRubyが動作しているため。
OPT_CALL_THREADED_CODE = 0
しかしvm_exec_coreなる定義はOPT_CALL_THREADED_CODEのifdefで分岐した上でvm_exec.cに2つある。
vm_opts.h というVMの最適化オプションが定義されたヘッダーファイルでこれはデフォルトで0になっているため、無効になっている。
そのため、こっちのvm_exec_coreの定義が使われることになる。
https://github.com/ruby/ruby/blob/v2_4_1/vm_exec.c#L47-L127
OPT_DIRECT_THREADED_CODE = 1
INSN_DISPATCHが呼ばれるとTC_DISPATCHというマクロが実行される。
TC_DISPATCHの定義はOPT_DIRECT_THREADED_CODEのifdefで分岐していて、デフォルトで1のため、こっちが使われる。TC_DISPATCHの詳細は後述。
なおダイレクトスレッデッドコード周りの説明はるびまに書いてある。
http://magazine.rubyist.net/?0008-YarvManiacs
insns.defはどう展開されるか
vm_exec_coreの中で "vmtc.inc" と "vm.inc" がincludeされていた。
これらのファイルはCRubyのビルド時にMakefileのこの定義から生成される。
https://github.com/ruby/ruby/blob/v2_4_1/Makefile.in#L531-L538
つまり tool/insns2vm.rb というRubyのコードからinsns.defを読み、これらのコードを生成している。
vmtc.inc
vmtc.inc.tmpl というファイルをベースに、以下のようなコードが生成される。
static const void *const insns_address_table[] = {
LABEL_PTR(nop),
LABEL_PTR(getlocal),
LABEL_PTR(setlocal),
LABEL_PTR(getspecial),
// ...
};
LABEL_PTRは以下のdefineにより定義されている。
#define LABEL_PTR(x) &&LABEL(x)
#define LABEL(x) INSN_LABEL_##x
るびまにちょっと説明がある。
&&ラベル名、でラベルを値として扱います。その値としてのラベルにジャンプするには goto *value と記述します。
そして実際のラベルは以下のINSN_ENTRYマクロによって、次に説明するvm.inc内で作られる。
#define INSN_ENTRY(insn) \
LABEL(insn): \
INSN_ENTRY_SIG(insn); \
vm.inc
vm.inc.tmplというファイルをベースに、以下のようなコードが生成される。
INSN_ENTRY(nop){
{
DEBUG_ENTER_INSN("nop");
ADD_PC(1+0);
PREFETCH(GET_PC());
#define CURRENT_INSN_nop 1
#define INSN_IS_SC() 0
#define INSN_LABEL(lab) LABEL_nop_##lab
#define LABEL_IS_SC(lab) LABEL_##lab##_##t
COLLECT_USAGE_INSN(BIN(nop));
{
#line 40 "../ruby/insns.def"
/* none */
#line 32 "vm.inc"
#undef CURRENT_INSN_nop
#undef INSN_IS_SC
#undef INSN_LABEL
#undef LABEL_IS_SC
END_INSN(nop);}}}
INSN_ENTRY(getlocal){
{
VALUE val;
rb_num_t level = (rb_num_t)GET_OPERAND(2);
lindex_t idx = (lindex_t)GET_OPERAND(1);
DEBUG_ENTER_INSN("getlocal");
ADD_PC(1+2);
PREFETCH(GET_PC());
#define CURRENT_INSN_getlocal 1
#define INSN_IS_SC() 0
#define INSN_LABEL(lab) LABEL_getlocal_##lab
#define LABEL_IS_SC(lab) LABEL_##lab##_##t
COLLECT_USAGE_INSN(BIN(getlocal));
COLLECT_USAGE_OPERAND(BIN(getlocal), 0, idx);
COLLECT_USAGE_OPERAND(BIN(getlocal), 1, level);
{
#line 60 "../ruby/insns.def"
val = *(vm_get_ep(GET_EP(), level) - idx);
#line 58 "vm.inc"
CHECK_VM_STACK_OVERFLOW_FOR_INSN(VM_REG_CFP, 1);
PUSH(val);
#undef CURRENT_INSN_getlocal
#undef INSN_IS_SC
#undef INSN_LABEL
#undef LABEL_IS_SC
END_INSN(getlocal);}}}
// ...
INSN_ENTRY(xxx) { ... }
がYARV insnの種類の数、つまり94個続く。
INSN_ENTRYの最後はEND_INSNが必ずついているが、その定義は以下のようになっている。
#define END_INSN(insn) \
DEBUG_END_INSN(); \
TC_DISPATCH(insn);
TC_DISPATCHは何をしているのか
さて先ほどチラと見たが、TC_DISPATCH (多分 threaded code dispatchの略) の定義はこうなっている。
#define TC_DISPATCH(insn) \
INSN_DISPATCH_SIG(insn); \
goto *(void const *)GET_CURRENT_INSN(); \
;
現在のinsnのラベルを取得してgotoしてそうなコードである。実際にGET_CURRENT_INSNが何をしているか見ていく。
#define GET_CURRENT_INSN() (*GET_PC())
#define GET_PC() (COLLECT_USAGE_REGISTER_HELPER(PC, GET, VM_REG_PC))
COLLECT_USAGE_REGISTER_HELPERの定義は、
#if VM_COLLECT_USAGE_DETAILS
#define COLLECT_USAGE_REGISTER_HELPER(a, b, v) \
(COLLECT_USAGE_REGISTER((VM_REGAN_##a), (VM_REGAN_ACT_##b)), (v))
#else
#define COLLECT_USAGE_REGISTER_HELPER(a, b, v) (v)
#endif
なお VM_COLLECT_USAGE_DETAILS はデフォルトで0になっている。つまり、GET_CURRENT_INSN()
を展開すると、(*(VM_REG_PC))
になるということだ。
さらに見ていくと、
#define VM_REG_PC (VM_REG_CFP->pc)
#define VM_REG_CFP (reg_cfp)
になっていて、 reg_cfp は vm_exec_core 内で INSN_DISPATCH();
よりも前で宣言されており、
DECL_SC_REG(rb_control_frame_t *, cfp, "si");
であり、
#define DECL_SC_REG(type, r, reg) register type reg_##r __asm__("r" reg)
なので、展開すると実際の宣言は以下のような感じになっている。
register rb_control_frame_t * reg_cfp __asm__("cfp" "si");
rb_control_frame_tの定義は以下のようになっており、
typedef struct rb_control_frame_struct {
const VALUE *pc; /* cfp[0] */
VALUE *sp; /* cfp[1] */
const rb_iseq_t *iseq; /* cfp[2] */
VALUE self; /* cfp[3] / block[0] */
const VALUE *ep; /* cfp[4] / block[1] */
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */
} rb_control_frame_t;
まあつまりvm_exec_core内に上記の構造体の変数reg_cfp
があり、TC_DISPATCH
はその内のconst VALUE *
なpc (program counterだと思う) の値からラベルを取得し、そこにgotoしている。
なおこの値は、以下のような定義によりADD_PC
によって操作されており、各INSN_ENTRYの中には必ずこれが入っているため、常に次の命令にgotoし続けることがわかる。
#define SET_PC(x) (VM_REG_PC = (COLLECT_USAGE_REGISTER_HELPER(PC, SET, (x))))
#define ADD_PC(n) (SET_PC(VM_REG_PC + (n)))
次はreg_cfp
の中身はどこで作られ、どういうものが入っているか見ていきたいところだが、疲れたのでこのへんにしておく。