YARV instruction dispatch

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の詳細は後述。

https://github.com/ruby/ruby/blob/v2_4_1/vm_exec.h#L102-L105

なおダイレクトスレッデッドコード周りの説明はるびまに書いてある。
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
https://github.com/ruby/ruby/blob/v2_4_1/template/vmtc.inc.tmpl

vmtc.inc.tmpl というファイルをベースに、以下のようなコードが生成される。

```c
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
https://github.com/ruby/ruby/blob/v2_4_1/template/vm.inc.tmpl

vm.inc.tmplというファイルをベースに、以下のようなコードが生成される。

```c
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();`よりも前で宣言されており、

```c
DECL_SC_REG(rb_control_frame_t *, cfp, "si");
```

であり、

```
#define DECL_SC_REG(type, r, reg) register type reg_##r __asm__("r" reg)
```

なので、展開すると実際の宣言は以下のような感じになっている。

```c
register rb_control_frame_t * reg_cfp __asm__("cfp" "si");
```

rb_control_frame_tの定義は以下のようになっており、

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