あなとみー おぶ 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のローカル変数の心だー

多分続く