LoginSignup
5
2

More than 5 years have passed since last update.

YARV instruction dispatch

Posted at

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の中身はどこで作られ、どういうものが入っているか見ていきたいところだが、疲れたのでこのへんにしておく。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2