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

かなり間があいちゃったけど、予告通りgccの拡張asmをつかったネイティブコードの呼び出し部分の説明を行います。gccの拡張asmはインラインアセンブラのくせに最適化して書いたコードのようにアセンブラを生成してくれないことがあるばかりか、変数が割り当てられたレジスタとかも警告なしで破壊してくれます。自分の足を撃つことができる言語っていうフレーズが良く使われていますが、血管や神経を避けて常に足に向けて撃つのが拡張asmです。

拡張asmは文法が複雑なんですが、さすがにこんなお化けを相手にする人は優秀な人が多くて(ここに例外がいるが・・・)、解説記事はどれも分かりやすいです。たとえば、これとか http://d.hatena.ne.jp/wocota/20090628/1246188338

それでは、具体的に解説していきます。拡張asmやX86の命令の説明をその都度行っていこうかと思います。ネイティブコード呼び出し部分だけなので拡張asmやX86チュートリアルだと思った人は(そんな人いるのか?)ごめんなさい。

      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");

mrubyのJITはよく使う、regsとpc(正確にはpcへのポインタのppc)をレジスタに割り当てています。それが、それぞれecx, ebxなんですが、前半の2行はその設定をしています。
mov命令はレジスタやメモリに値を設定するもっとも基本的な命令です。ややこしいことにGNUのツールとVCやMASMなどのMSのツールではオペランドの順番が違うのですが、ここでは、 mov %0, %%ecxとすると、%0 → ecx という意味になります。つまり、右側が代入先になります。ちなみに、XbyakはMS系(正確にはIntel系)なので、右は代入する値です。この先するであろうコード生成では逆になりますので注意してください。

\n\tは、herumiさんに教わったテクニックでこうすると、gcc -Sで得られる生成したアセンブラが見やすくなるとのことです。

2つ目のコロン"g"(regs), "g"(ppc)が設定する値です。"g"って何?と思われるかもしれませんが、アセンブラオペランド(この場合%0, %1)に"g"という制約がかかりますよという意味です。"g"はメモリかレジスタという意味になります。つまり、"mov %0, %%ecx\n\t"で%0のところにはメモリかレジスタが来るという意味になります。regsはレジスタかスタック上にespかebpのオフセット参照で表現できますからそのまま1命令が生成されることが期待できます。
一方、"d"(status)はオペランドにedxが来るという意味になります。statusがedxに割り当てられていることはもちろん期待できないので、mov命令でedxに格納してmov %2, -0x4(%%esp)を実行します。edxなのは後で説明するようにネイティブコードを呼び出すことで壊れるからです。
mov %2, -0x4(%%esp)でスタックトップにstatus(第4回参照)が入ります。これがあれば、ここから手繰ることで、ecx, ebxの情報はなくてもいいのですが、速度向上のために割り当てています。

ちなみにpushを使わないのはespを動かすとローカル変数がアクセスできなくなる場合があるからです。最適化なしだと動くのでものすごくはまります。

                   : "%ecx",
		     "%ebx",
		     "memory"

は壊れるレジスタを列挙します。ここに列挙しておくと、ecxとかにレジスタ変数が割り当てられていたとしても壊されたと認識してスタック上にあるオリジナルから再ロードするとか予期に計らってくれます。下手に自分でスタックに退避しようとすると必ずはまります。大部分は動くのに特定のバージョンであるオプションをつけると動かないとか平気であります。
"memory"は文字通りメモリーが壊れることを示します。こうしておくと、レジスタにキャッシュしてあったメモリの内容も念のため再ロードしたりしてくれます。

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

ネイティブコードを実際に呼び出します。espの値が合わないと動かないのでcallの前後でつじつまを合わせます。callのところでciというローカル変数をアクセスしているけどちゃんと動いてますね。なぜだろう??たぶん、subの前でci->entryをレジスタに入れているからかな?

ネイティブコードを呼び出して、返ってくると次の値が返ってきます。

  • eax 次に実行するmrb_run中のラベルのアドレス
  • edx  今実行していたネイティブコードの後始末処理の先頭アドレス

どちらも今後の肝になるので、次回以降にもいろいろ出てくると思います。

eax, edxの値をCからもアクセスできるようにCのローカル変数に格納します。

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

これで、拡張asmの説明は終わりです。次回はさらにプログラムの続きを見ていきたいと思います。

続く

あなとみー おぶ 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講座になりそう気もしますが次回に続きます。

たぶん、続く 

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

今回は前回の予告通りstatusとは何かからです。

何度もmrubyには処理系の状態がmrb_state構造体でまとまっていていいよという話をしていますが、実はmrb_state構造体に入っている状態だけがすべてではないのです。mrb_stateに入っていない状態は何かというとmrb_runのローカル変数で実現されているVMの状態です。その1で説明した通り、Tracing JITではプログラムすべてをコンパイルするわけではないので途中でVMに戻る必要があります。戻った時にVMの状態であるmrb_runのローカル変数の内容も整合性が取れている必要があります。ところが、何もしないとネイティブコードからはどこにmrb_runのローカル変数の情報があるのか分からないので書き換えることはできません。*1

このような問題を解決するためにmrubyのJITではstatusという変数を用意しました。どういうものかはstatusの初期化処理をみれば一目瞭然です。

  mrbjit_vmstatus status = {
    &irep, &proc, &pc, &pool, &syms, &regs, &ai, 
    optable, gototable, &prev_jmp
  };

つまりローカル変数のアドレスをメンバーにした構造体です。それぞれの変数は型が違いますので配列ではなく構造体です。構造体の定義は、include/mruby/irep.hにあってこんな感じです。

typedef struct mrbjit_vmstatus {
  mrb_irep **irep;
  struct RProc **proc;
  mrb_code **pc;
  mrb_value **pool;
  mrb_sym **syms;
  mrb_value **regs;
  int *ai;
  void **optable;
  void **gototable;
  jmp_buf **prev_jmp;
} mrbjit_vmstatus;

ポインタのポインタになるので*がいっぱいついています。アドレスを取るとレジスタに割り当てられなくて遅くなるのでは?と思う人もいるかもしれませんが、そうでしょうけど大部分のところはネイティブコードで実行するので大丈夫じゃないかと思います。
それぞれのメンバーの紹介をします。

  • irep 現在実行中のメソッド・ブロックの各種情報(procのメンバーのコピー)
  • proc 現在実行中のメソッド・ブロックのProcオブジェクト
  • pc 現在実行中のVMの命令を指すポインタ
  • pool 定数テーブル(irepのメンバーのコピー)
  • syms シンボルテーブル(irepのメンバーのコピー)
  • regs レジスタの配列(irepのメンバーのコピー)
  • ai mrb_run開始時のアリーナのインデックス(GC関係の話)
  • opttable 命令ごとのラベルのアドレスの配列
  • gototable opttableに入っていないラベルのアドレスの配列
  • prev_jmp mrb_run開始時のmrb->jmp 例外時にスタック巻き戻しに使う

statusをネイティブコードには引数として渡してやるわけです。そうすることでコンパイラの変更やプログラムの変更を心配することなくVMの状態が書き換えられるわけです。ちなみに、mrb_state構造体は引数として渡しません。使わないの?というとそうではなく、JITコンパイラが動いている間にmrb_state構造体のアドレスは変わらないので直接そのアドレスをアクセスするように命令を生成してしまいます。

というところで、vm.cでの話は終わり。*2

次回「mrubyのjitの中心部jit.cの説明」私は、生き延びることができるか?

*1:コンパイラの生成コードを解析してローカル変数のスタック上の位置やレジスタ変数が保存されている場所を突き止めるのは可能です。ただし、変数を増やしたりコンパイラやオプションが変わっただけで動かなくなることでしょう

*2:実は少し残っていますが説明しないかもしれない。vm.cをmrbjit_で検索してもらえば分かります。static関数を呼び出すための苦肉の策です

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

今回からプログラムの説明です。今回はmrubyのVMの命令実行部分、vm.cのmrb_runにどうパッチを当てているかを見てみます。mrb_runは大まかに言ってこんな感じになってます。

  1. 初期化
  2. 命令取出し
  3. 取出した命令に当たる処理にジャンプ
  4. 命令の実行
  5. 2に戻る

3のジャンプはswitch/caseの場合とgotoの場合があります。mrubyのJITはgotoの場合( DIRECT_THREADED)の場合だけをサポートします。その理由は後で(珍しくこの回の中で)説明します。
JITを組み込んだ場合次のようになります。

  1. 初期化
  2. 命令の取り出し
  3. 命令がコンパイルされていればそれを実行する。全部コンパイルするわけではないので戻ってくる。この場合、2で取出した命令とは違うので再度命令を取出して5に飛ぶ
  4. もし可能ならば命令をコンパイルする。
  5. 取出した命令に当たる処理にジャンプ
  6. 命令の実行
  7. 2に戻る

それでは、実際どうmrb_runに手を入れたのか説明します。
あまり既存のmrubyに手を入れるとmrubyが変更した時にconflictが起きまくって大変なので出来る限り既存のmrubyには手を入れたくない。これがmrb_runに手を入れる最優先事項です。
あと、注意すべきはジャンプするのは命令の実行だけではないということです。どういう意味かというと、mrb_runでは例外が発生するとエラーハンドラにgotoで飛ぶのです。例えばL_RAISEで検索してみるといいでしょう。mrubyのJITコンパイルしたコードも例外が発生したらちゃんとハンドラに飛んでくれないと困るわけです。種明かしをすると、DIRECT_THREADでなければならないのはこれをサポートするためです。
そんなこんなで考えたのが次のような変更です。

mrb_runで使われているマクロにNEXT,JUMPというものがあります。これは、命令を取り込んでジャンプするものです。元の定義はこんな感じです。

#define NEXT i=*++pc; goto *optable[GET_OPCODE(i)]
#define JUMP i=*pc; goto *optable[GET_OPCODE(i)]

これをこう変えます

#define NEXT ++pc;gtptr = mrbjit_dispatch(mrb, &status);i=*pc; goto *gtptr
#define JUMP gtptr = mrbjit_dispatch(mrb, &status);i=*pc; goto *gtptr

ははーん、mrbjit_dispatchってのがコンパイルとかネイティブコード実行とかやってるんだなと分かると思います。おそらく分からないのは、mrbjit_dispatchって何を返しているの?かラベルのアドレスを返しているんだろうけどgoto *optable[GET_OPCODE(i)]でいいじゃん?ということだと思います。あとstatusって?というのもあると思うけどそれは後で。

mrbjit_dispatchは確かに次ジャンプするラベルのアドレスを返します。多くの場合は*optable[GET_OPCODE(i)]です。でも、そうじゃない場合もあります。ネイティブコードが例外を発生してL_RAISEに飛びたい場合もあります。この場合はL_RAISEが返ってきます。これでgotoで飛ぶ例外ハンドラとかも対応出来るわけです。

ところで、なんでgotoのラベルのアドレスを知っているの?と思われるかもしれませんが答えはstatusに中にあるからです。statusって何なの?というのはコードを見るとすぐわかるのですがなんでこんなことをするのかは説明が要りそうなので、次回説明します。

次回は変数に関するお噂、mrb_runのローカル変数の心だー

多分続く

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

前回の1.10001・・・は最初に証明された超越数の10倍でした。
私が積読してあることで名高い「人月の神話」には、つぎのようなくだりがあります。

私にフローチャートを見せられて、テーブルを見せないとしたら、私はずっと煙に巻かれたままになるだろう。逆にテーブルが見せてもらえるなら、フローチャートは大抵必要なくなる

そういうことで、mrubyのJITの解説第二回目はデータ構造です。

よく言われることですが、mrubyには大域変数が無く代わりにstate構造体(mrb_state)を定義してそのポインタをほぼすべての関数に引数として渡すようにしています。このような構造はVMを複数作れるとか嬉しいことが多いのですが、mrubyのJITに関しても恩恵にあずかっています。この恩恵については後で説明しますが(伏線回収できるかな?どきどき)、mrb_stateにちゃっかりJITで使う変数も忍ばせています。

  mrbjit_comp_info compile_info; /* JIT stuff */

なんと、忍ばせているのは1つだけ。なかなか謙虚ですね。ただ、これは構造体でこの中にいっぱい(といっても現状はそれほどでもない)メンバーがいます。

typedef void * mrbjit_code_area;
typedef struct mrbjit_comp_info {
  mrb_code *prev_pc;
  mrbjit_code_area code_base;
  int disable_jit;
  int nest_level;
} mrbjit_comp_info;

それぞれ説明します。ちなみにこの2つの構造体はinclude/mruby.hにあります。

  • prev_pc これはいまコンパイルしようとしているVMの命令の前に実行した命令のポインタが入っています。なんでこんなのが必要かというと、逐次実行で実行した場合とジャンプで飛んできた場合ではレジスタの設定状況とかが違う可能性が高いので別の命令列として扱うからなのですが、詳しくはコンパイルの処理のところで説明します(伏線広げすぎて大丈夫かなーどきどき)。
  • code_base これはXbyakのオブジェクト(MRBJitCode)へのポインタです。ここが管理するC++のオブジェクトが管理する記憶領域にコンパイルされたコードが格納されます。mrubyはC++ではなくCで書かれているのでmrubyを出来る限り変えないためにmruby側からはvoidへのポインタとなっています。C++側(jitcode.cc)でキャストして使います。
  • disable_jit これはコンパイルするかしないかを制御するフラグでnon-0だとコンパイルしません。なんでこんなのがいるかというと、mrb_runが再帰的に呼び出されると想定しているスタックレイアウトが狂ってバグるからです。再帰的に呼び出されるのはほとんどが短くて時間のかからない処理だと思うので多分これで問題ないです。
  • nest_level これは現在コンパイル中のメソッドのレベルを表します。Tracing JITだとメソッドのsendやenterがコンパイルされなくてreturn時だけコンパイルするような場合もあり得ますのでそういう場合にreturnをコンパイルしないようにするためのものです。

こんな感じです。思ったより書くのに時間がかかって全部書ききれなかったのですが、用事があるのでまたあとで続きを書きます。
用事が終わったので続きます。ビール美味しかった。

次に、irep関連の構造体を見てみます。mruby全体の情報はmrb_statusにあるわけですが、ここのメソッドの情報、例えば命令列(iseq)や定数テーブル(pool)はmrb_irepという
構造体に格納されます。mrb_irepの配列がmrb_statusのメンバーになっています。

mrb_status の一部です。

  struct mrb_irep **irep;
  size_t irep_len, irep_capa;

で、この命令列mrb_irepにもコンパイラで使う情報が忍ばせてあります。このmrb_irep他の定義はinclude/mruby/irep.hにあります。

  /* JIT stuff */
  int *prof_info;
  mrbjit_codetab *jit_entry_tab;

この二つのメンバーを説明します。

  • prof_info 命令ごとの実行回数を表します。ポインタなのは命令列の長さだけ配列を確保して使うからです。1000を超えるとコンパイルしてネイティブコードが実行されますので1002くらいまでしか更新されません。プロファイラの代わりにはならないわけです。カバレージなら取れそうです。
  • jit_entry_tab 命令ごとにネイティブコードのアドレスを表します。実はそれだけではなくCPUレジスタの使用状況とかXbyakで使うコード領域を複数持つためのフィールドとか持っているのですがまだ未使用です。

ここまでirep.hで定義されています。

jit_entry_tabの構造体mrbjit_codetabは、include/mruby/jit.hに定義されていて次のような構造をしています。

typedef struct mrbjit_codetab {
  int size;
  mrbjit_code_info *body;
} mrbjit_codetab;

sizeとあるので複数あることが分かります。ネイティブコードのアドレスなどの情報(code_info)は1つのVM命令に一つではなく複数あり得ます。なぜかと言えば、直前に実行した命令によってレジスタなどの内容が変わるため(前にも説明しましが)、生成されるコードが違う可能性があるからです。現状では直前に実行した命令と今実行中のメソッドの呼び出し元のアドレスで別々のcode_infoを割り当てています。ただし、呼び出し元のアドレスで別々のcode_infoになるのはメソッドをインライン展開する場合だけです。Tracing JITは簡単にインライン展開出来るのですが、やりすぎるとネイティブコードが膨れて却って遅くなるので特定の条件(現在は5命令以下のメソッド)だけで行っています。

code_infoの内容を示します。

typedef struct mrbjit_code_info {
  mrbjit_code_area code_base;
  mrb_code *prev_pc;
  mrb_code *caller_pc;
  void *(*entry)();
  mrbjit_varinfo dstinfo;	/* For Local assignment */
  mrbjit_inst_spec_info inst_spec;
  int used;
} mrbjit_code_info;

メンバーがたくさんありますが、現状余り使っていないです。将来最適化とかする時はフル活用して、メンバーも大幅に増えることでしょう。

使っているメンバーを紹介します。

  • prev_pc 前に実行した命令のアドレスです。code_info検索用
  • caller_pc 現在実行中のメソッドの呼び出し元のアドレスです。code_info検索用
  • entry ネイティブコードのエントリーアドレスです。説明するスタックフレームを整えたりした後、このアドレスをcallするとVMと同じ動きをします。
  • used このcode_infoが使われているかどうか。mallocの回数を節約するためcode_infoはまとめてアロケーションします。そのため、使っているかのフラグが必要です。

使っている構造体はこんな感じです。最適化とか始めると構造体の数の桁が変わることでしょう。早くそうなるといいですね(他人事)、ということで今回は終わり。

たぶん、次回もあることでしょう。予定は未定ですが。

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

 謝辞を忘れていました。mrubyのJITXbyak(http://homepage1.nifty.com/herumi/soft/xbyak.html)を使わせてもらっています。今まで、C言語の生成、LLVMRubyで書いた自作アセンブラなどでコンパイラを使っていましたが、Xbyakは一番使いやすいです。
某出版会(http://tatsu-zine.com/books/llvm)には大変申し訳ないのですが、個人的には速度とか移植性を考えないコンパイラならLLVMよりXbyakの方がお勧めです。移植性とか最適化したいとかの話になると途端にXbyakはハードルが上がりますが…
作者のherumiさんにはmrubyのJITを試し戴いてデバッグの協力まで戴きました。
https://github.com/miura1729/mruby/issues/1
本当にありがとうございます。
ちなみにmrubyのJITは32bit X86専用です。64bitのLinuxでも-m32オプションで動きます。

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

ふと思い立ってものすごく影の薄いmrubyのJITの内部構造を説明することにしました。mrubyのJITは正式名称がないというとてもかわいそうなmrubyのフォークですが、オリジナルのmrubyの1〜3割(倍じゃないのに注意)速いようです。ここにあります、 https://github.com/miura1729/mruby

さて、mrubyのJITはTracing JITなるものを使っています。第一回目はTracing JITの説明をしたいと思います。コードの解説はしないので、そういうのが読みたい人はTwrtter (@miura1729)かコメントで続きはよとつっついてください。

Tracing JITは今やLuaJIT, Pypyをはじめとするいろんな処理系で使われていますが、あまり解説記事はないようです。ただ、http://dodgson.org/omo/t/?date=20080510 が素晴らしくて他に書きようがないという話かもしれません。これを読んでもらえればいいのですが、mrubyのJITはもっと手を抜いているのでmrubyのJITに即して説明します。

mrubyのJITはこんな感じで動きます。

  • mrubyのVM本体(mrb_run)でどの命令を実行しようかなー?と選ぶ直前のタイミングで制御を取り上げる
    • これから実行しようとするVMの命令が既にコンパイルされていたらそれを呼び出す
    • コンパイルされていないなかったら、その命令に対応する機械語列を生成する
    • 何事も無かったかのようにmrb_runに戻る

こんだけです。ただ、すべての命令列をコンパイルすると、初期化でコンパイルしたけど二度と通らないという悲しいことになりますので、命令が実行した回数を数えて一定数(現状1000回)以上になったらコンパイルするようにしています。

とっても単純でいいのですが、いろいろ疑問もわくことと思います。

  • コンパイルされたコードを呼び出した後戻ってきたらちゃんと続きが実行出来るの?
  • VMで条件分岐命令が出てきたり、違うオブジェクトが変数に入っていた時は?
  • これって遅くない?

では答えていきます。

 コンパイルされたコードを呼び出してその後、VMがちゃんと続きが出来るかということですが、そうなるように作っています(答えになってない気もする)。LuaJITとかだとVMで更新しないといけない情報をコンパイル時に管理してVMに戻るタイミングで更新しているようです。一方、mrubyのJITは常にmrubyのVMの状態をコンパイルされたコードで更新しながら実行しています。簡単ですが、変数をレジスタに割り当てるとか出来なくて遅いです。

 条件分岐命令とかはどうするかですが、ここで説明するのは難しいのでまた次回以降に説明します。キーワードはガードです。気になる人は、http://dodgson.org/omo/t/?date=20080510 を読んでください。

mrubyは手抜きなので輪を掛けてますが、Tracing JITは普通のAOTより遅くなると思いっます。じゃあ、化け物LuaJITは?と思われると思いますが、あれは二段熟カレーならぬ二段コンパイルをしているはずです。特に重いと思った所をじっくりコンパイルします。Pypyはどうやってるだろう?

そういうことで今回はおわり。まだ勢いがあるので第二回はあることでしょう
続く