あなとみー おぶ mrubyのJIT(その5)

いよいよここまで来たか。ということで、今回はjit.cの中でも中心となる、mrbjit_dispatchの説明をします。大まかな動きについては大体説明したと思いますので、プログラムを細かく見ていきます。

void *
mrbjit_dispatch(mrb_state *mrb, mrbjit_vmstatus *status)

戻り値と引数。これは、前回説明した通りですね。

  mrb_irep *irep = *status->irep;
  mrb_code **ppc = status->pc;
  mrb_value *regs = *status->regs;
  size_t n;
  mrbjit_code_info *ci;
  mrbjit_code_area cbase;
  mrb_code *prev_pc;
  mrb_code *caller_pc;
  void *(*entry)() = NULL;
  void *(*prev_entry)() = NULL;

変数の宣言です。statusをいちいち参照していると遅いのでショートカットという意味の変数もあります。そういうのはここで初期化しておきます。

  if (mrb->compile_info.disable_jit) {
    return status->optable[GET_OPCODE(**ppc)];
  }

disable_jitが真の時はコンパイルもネイティブコードの実行もせずに即座に終わります。

  prev_pc = mrb->compile_info.prev_pc;

prev_pcは前回ここを通った時のpcの値です。どこから飛んできたのかで別々のcode_infoを割り当てるためにこうしています。code_infoはエントリーアドレスとかレジスタの使用情報などが入った構造体で、簡単に言うと、code_infoが異なるとたとえmrubyのVMのレベルでは同じ命令でも全然別の機械語コードになります。

  if (irep->ilen < NO_INLINE_METHOD_LEN) {
    caller_pc = mrb->ci->pc;
  }
  else {
    caller_pc = NULL;
    mrb->compile_info.nest_level = 0;
  }

メソッドをインライン化するかを判定します。NO_INLINE_METHOD_LENはデフォルトでは5でgetter/setterをインライン化するのを想定しています。caller_pcはメソッドの呼び出し元のpcの値で、インライン化の時は呼び出し元毎にcode_infoを用意します。インライン化しない場合はNULLにして呼び出し元関係なく共有します。インライン化しない場合はnest_levelを0にしてOP_RETURNをコンパイルしないようにします。つまりメソッドの終わりに必ずVMに戻るわけです。OP_SENDでは呼び出し元のアドレスを保存しないため、callinfoを共有した場合、呼び出し元が分からなくなります。詳しくは、OP_SEND, OP_RETURNのコード生成の時に説明します。ちなみに、OP_SEND, OP_RETURNを実装して正月がつぶれました。
mrb->ci->pcはcallerの呼び出し元を表します。mrb->ciはメソッド・ブロックの呼び出し履歴が入っています。

  cbase = mrb->compile_info.code_base;
  n = ISEQ_OFFSET_OF(*ppc);
  if (prev_pc) {
    ci = search_codeinfo_prev(irep->jit_entry_tab + n, prev_pc, caller_pc);
  }
  else {
    ci = NULL;
  }

ここで、code_infoを検索します。これを書いていて、mrb_runではciはcallinfoなので紛らわしいなと気付きました。直す可能性大です。
nはppcで指す命令がメソッドの先頭から何番目かを表しています。search_codeinfo_prevはprev_pc, caller_pcが一致するcode_infoを探すための関数です。アルゴリズムはリニアサーチで効率が悪いのですが、普通は要素数が少ないので問題ないと思います。2つキーがあるので効率を高めるのは結構面倒そうです。

  if (ci) {
    if (cbase) {
      if (ci->used > 0) {
	mrbjit_gen_jump_block(cbase, ci->entry);
	cbase = mrb->compile_info.code_base = NULL;
      }
    }
    

code_info(ci)が見つかったかどうかチェックします。見つかった場合はそれにちゃんとネイティブコードが入っていれば既にコンパイル済みとうことです。いろんな都合でネイティブコードが入っていない場合もあって、これはci->usedでチェックします。これが0より大きいとコンパイル済みでネイティブコードが入っています。
cbaseはXbyakのオブジェクトのポインタでこれがNULLではない場合はコンパイルモードというわけです。

そういうわけで内側の条件判定

      if (ci->used > 0) {
	mrbjit_gen_jump_block(cbase, ci->entry);
	cbase = mrb->compile_info.code_base = NULL;
      }

コンパイル中に既にコンパイル済みのコードが出てきたら、ジャンプ命令(X86機械語の)を生成してコンパイル済みのコードと合流してコンパイルを終わるという処理になります。

    if (cbase == NULL && ci->used > 0) {
      void *rc;
      prev_pc = *ppc;

さて、コンパイル中ではなく、ネイティブコードがある、いつネイティブコードを呼び出すの今でしょう、ということでネイティブコードを呼び出す処理が続きます。

      asm volatile("mov %0, %%ecx\n\t"
		   "mov %1, %%ebx\n\t"
		   "mov %2, -0x4(%%esp)\n\t"
		   :
		   : "g"(regs),
		     "g"(ppc),
		     "d"(status)
		   : "%ecx",
		     "%ebx",
		     "memory");

      asm volatile("sub $0x4, %%esp\n\t"
		   "call *%0\n\t"
		   "add $0x4, %%esp\n\t"
		   :
		   : "g"(ci->entry)
		   : "%edx");

      asm volatile("mov %%eax, %0\n\t"
		   : "=c"(rc));
      asm volatile("mov %%edx, %0\n\t"
		   : "=c"(prev_entry));

たぶん、何やっているか分からないでしょう。私も分かってもらえるような説明が出来る自信はないのですが、次回頑張ります。これだけは、言っておきたいので書きます。gccの拡張asmを使う場合の座右の銘「下手な考え休むに似たり」。
それでは、次はgccの拡張asm講座になりそう気もしますが次回に続きます。

たぶん、続く