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

今回はオブジェクト作成のメソッドnewのインライン化のお話です。newのインライン化はそれほど規模が大きくないのですが、mrubyのJITの大きな転換点になっています。

前回取り組んだytljitがRuby型推論(ブロックや要素ごとに違う型が格納できる配列などを含む)やオブジェクトのスタック割り付けなどを行って複雑すぎて手に負えなくなっちゃったことがありました。その反省を踏まえて、できるだけオーソドックスに分かりやすく気を使っていました(結果どうなったかは知らない)。

ところが、aoベンチでオブジェクトの生成がボトルネックっぽいと分かったことでnewをインライン化を初めて、検討の結果またも悪魔に魂を売り渡してしまいました。その、技法とは「バイトコードの書き換え」です。

newの難しいところはオブジェクトを生成した後、initilaizeメソッドを呼ばないといけないところです。簡単じゃないかと思われるかもしれませんが、mrubyではnewでのinitilizeメソッドはmrb_run直接ではなく、vm.cで定義されているmrb_funcall_with_block経由で呼ばれます(結局mrb_runで実行されるのですが)。そうすると、詳しい理由は避けますがJITコンパイルはうまくいきません。そういうことで、newをコンパイルしてその機械語を実行するときinitizlzeメソッドの機械語コードが生成されていないのです。mrb_funcall_with_blockを呼び出してインタープリタでinitilizeメソッドを実行してもいいのですが、それでは面白くないです。

いろいろ考えて、newをmrb_instance_allocで得た初期化していないオブジェクトをレシーバーにしてinitializeメソッドを呼び出す動作にコンパイルしたらどうだろうと考えました。このようにすることで、initalizeはmrb_funcall_with_block経由ではなく、mrb_run直接実行になります。前述のとおり、initializeメソッドは機械語が生成されていないので、メソッド呼び出しのためのcallinfoを構築したら、即座にVMに戻るコードを生成するようにしました。

これで、基本的にうまくいったのですが1つ問題があります。newメソッドは生成したオブジェクトを返すのですが、initializeメソッドは生成したオブジェクト(initializeメソッドのself)を返すとは限らないことです。initializeメソッドから戻ったところで改めて生成したオブジェクトを返すようにすればいいのですが、そうするとinitializeメソッドを呼びっぱなしにできないのでcallinfo構築にコストがかかります。要は必ずinitializeメソッドで必ずselfを返してくれればいいのです。

戻り値を返すVMの命令はOP_RETURNなわけですが、

  OP_RETURN    R0

とすると現在のmrubyでは必ずselfを返します。つまり、OP_RETURNのAオペランドを0クリアすればよいのです。initializeメソッドの戻り値は使われることがないので副作用もありません。簡単に実装できて効果のあり、被害も少なそう、な命令書き換えをやらない手があるだろうか? ということで命令書き換えで実装しました。

コードを見てみましょう

mrb_value
MRBJitCode::mrbjit_prim_instance_new_impl(mrb_state *mrb, mrb_value proc,
					  mrbjit_vmstatus *status, mrbjit_code_info *coi)
{
  mrb_value *regs = *status->regs;
  mrb_irep *irep = *status->irep;
  mrb_code *pc = *status->pc;
  int i = *pc;
  int a = GETARG_A(i);
  int nargs = GETARG_C(i);

  struct RProc *m;
  mrb_value klass = regs[a];
  struct RClass *c = mrb_class_ptr(klass);

  m = mrb_method_search_vm(mrb, &c, mrb_intern(mrb, "initialize"));

  // TODO add guard of class
  
  // obj = mrbjit_instance_alloc(mrb, klass);
  push(ecx);
  push(ebx);
  mov(eax, *((Xbyak::uint32 *)(&klass) + 1));
  push(eax);
  mov(eax, *((Xbyak::uint32 *)(&klass)));
  push(eax);
  push(esi);
  call((void *)mrbjit_instance_alloc);
  add(esp, 3 * sizeof(void *));
  pop(ebx);
  pop(ecx);

  // regs[a] = obj;
  mov(ptr [ecx + a * sizeof(mrb_value)], eax);
  mov(ptr [ecx + a * sizeof(mrb_value) + 4], edx);

  if (MRB_PROC_CFUNC_P(m)) {
    CALL_CFUNC_BEGIN;
    mov(eax, (Xbyak::uint32)c);
    push(eax);
    mov(eax, (Xbyak::uint32)m);
    push(eax);
    CALL_CFUNC_STATUS(mrbjit_exec_send_c, 2);
  }
  else {
    /* patch initialize method */
    mrb_irep *pirep = m->body.irep;
    mrb_code *piseq = pirep->iseq;
    for (int i = 0; i < pirep->ilen; i++) {
      if (GET_OPCODE(piseq[i]) == OP_RETURN) {
	/* clear A argument (return self always) */
	piseq[i] &= ((1 << 23) - 1);
      }
    }
    
    /* call info setup */
    CALL_CFUNC_BEGIN;
    mov(eax, (Xbyak::uint32)c);
    push(eax);
    mov(eax, (Xbyak::uint32)m);
    push(eax);
    CALL_CFUNC_STATUS(mrbjit_exec_send_mruby, 2);

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, regs)]);
    mov(ecx, dword [eax]);

    gen_set_jit_entry(mrb, pc, coi, irep);

    gen_exit(m->body.irep->iseq, 1, 0);
  }

  return mrb_true_value();
}

細かく見てみます。

  // obj = mrbjit_instance_alloc(mrb, klass);
  push(ecx);
  push(ebx);
  mov(eax, *((Xbyak::uint32 *)(&klass) + 1));
  push(eax);
  mov(eax, *((Xbyak::uint32 *)(&klass)));
  push(eax);
  push(esi);
  call((void *)mrbjit_instance_alloc);
  add(esp, 3 * sizeof(void *));
  pop(ebx);
  pop(ecx);

  // regs[a] = obj;
  mov(ptr [ecx + a * sizeof(mrb_value)], eax);
  mov(ptr [ecx + a * sizeof(mrb_value) + 4], edx);

初期化していないオブジェクトの生成です。コメントのCのコードのほぼ直訳です。

  if (MRB_PROC_CFUNC_P(m)) {
    CALL_CFUNC_BEGIN;
    mov(eax, (Xbyak::uint32)c);
    push(eax);
    mov(eax, (Xbyak::uint32)m);
    push(eax);
    CALL_CFUNC_STATUS(mrbjit_exec_send_c, 2);
  }
  else {

mにはinitializeのProcオブジェクトが入っています。mがC関数だった場合の処理です。ここを通ることは意外と少ないです。配列とかもmrubyでinitializeが定義されていますし。

    /* patch initialize method */
    mrb_irep *pirep = m->body.irep;
    mrb_code *piseq = pirep->iseq;
    for (int i = 0; i < pirep->ilen; i++) {
      if (GET_OPCODE(piseq[i]) == OP_RETURN) {
	/* clear A argument (return self always) */
	piseq[i] &= ((1 << 23) - 1);
      }
    }
    

initializeがmrubyで定義されている場合の処理です。ここで、命令書き換えをやっています。

    /* call info setup */
    CALL_CFUNC_BEGIN;
    mov(eax, (Xbyak::uint32)c);
    push(eax);
    mov(eax, (Xbyak::uint32)m);
    push(eax);
    CALL_CFUNC_STATUS(mrbjit_exec_send_mruby, 2);

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, regs)]);
    mov(ecx, dword [eax]);

    gen_set_jit_entry(mrb, pc, coi, irep);

次にmrubyのメソッドを呼び出すcallinfoの構築です。説明はとても面倒なので省略します。

    gen_exit(m->body.irep->iseq, 1, 0);

最後にVMに戻ります。
泥臭い話が続きますが、次回も多分泥臭いです。コンパイラは開発が進むと泥臭くなるもののようです。

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

とってもお久しぶりです。ずいぶん間が空いてしまいましたが、新たな機能のプリミティブのインライン化が実装できたので再開したいと思います。あなとみー おぶ mrubyのJIT の開始当時オリジナルmrubyの数割速いとか遅くなるとか言っていましたが、現在大体のベンチマークでmrubyの2〜4倍の速度が出ます。その秘密が今回紹介するプリミティブのインライン化です。
Rubyはかなり基本的な機能もすべてメソッド呼び出しで実装されています。これは、必要に応じて動作をカスタマイズしたりして柔軟性をもたらしますが、速度的には大きなハンディになります。とくに、mrubyはメソッド呼び出しのオーバーヘッドが大きいので問題になります。
そこで、mrubyのJITでは良く使うメソッドについてはインライン化して速度を稼ぐようにしました。

インライン化を支えるコード

具体的なコードを見てみましょう。
これは、array.cの[]メソッドの定義ですが、こんな感じでプリミティブとしてインライン化したいメソッドを登録します。

  mrb_define_method(mrb, a, "[]",              mrb_ary_aget,         MRB_ARGS_ANY());  /* 15.2.12.5.4  */
  mrbjit_define_primitive(mrb, a, "[]", mrbjit_prim_ary_aget);

こうすると、JITコンパイル時に、mrbjit_prim_ary_agetを呼び出してインライン化するようになります。

mrbjit_define_primitiveの定義です。class.cにあります。

void
mrbjit_define_primitive_id(mrb_state *mrb, struct RClass *c, mrb_sym mid, mrb_func_t func)
{
  struct RProc *p;
  int ai = mrb_gc_arena_save(mrb);

  p = mrb_proc_new_cfunc(mrb, func);
  p->target_class = c;
  mrb_obj_iv_set(mrb, (struct RObject*)c, mid, mrb_obj_value(p));
  mrb_gc_arena_restore(mrb, ai);
}

void
mrbjit_define_primitive(mrb_state *mrb, struct RClass *c, const char *name, mrb_func_t func)
{
  mrbjit_define_primitive_id(mrb, c, mrb_intern(mrb, name), func);
}  

コードを見てわかるとおり、インスタンス変数領域にインライン化の関数をProcオブジェクトとして格納します。インスタンス変数は@, @@で普通始まるので通常のインスタンス変数とは衝突しません。まれに特殊目的で@,@@で始まらない変数を登録する場合もありますので注意が必要です。

実際にインライン化する場所を見てみましょう。jitcode.hのemit_sendです。

    if (MRB_PROC_CFUNC_P(m)) {
      prim = mrb_obj_iv_get(mrb, (struct RObject *)c, mid);
      mrb->vmstatus = status;
      if (mrb_type(prim) == MRB_TT_PROC) {
	mrb_value res = mrb_proc_ptr(prim)->body.func(mrb, prim);
	if (!mrb_nil_p(res)) {
	  return code;
	}
      }

      //puts(mrb_sym2name(mrb, mid)); // for tuning
      CALL_CFUNC_BEGIN;
      mov(eax, (Xbyak::uint32)c);
      push(eax);
      mov(eax, (Xbyak::uint32)m);
      push(eax);
      CALL_CFUNC_STATUS(mrbjit_exec_send_c, 2);
    }
    else {

呼び出そうとするメソッドがCで定義された場合の処理ですが、インライン化すると登録された場合は登録された関数を呼び出してインライン化するコードを生成します。生成に成功したら、nil以外を返すことになっているのでチェックして、その後のmrbjit_exec_send_cの呼び出しはキャンセルします。コード生成が失敗したら、nilを返して何事もなかったようにmrbjit_exec_send_cを生成して通常のメソッド呼び出し
シーケンスを実行します。

インライン化コードの例

では、実際にインライン化を行うコードを見てみましょう。primitive.ccでいろいろ定義していますが、その中で簡単なFixnum#succのインライン化を解説します。

mrb_value
MRBJitCode::mrbjit_prim_fix_succ_impl(mrb_state *mrb, mrb_value proc)
{
  mrbjit_vmstatus *status = mrb->vmstatus;
  mrb_code *pc = *status->pc;
  int i = *pc;
  int regno = GETARG_A(i);
  const Xbyak::uint32 off0 = regno * sizeof(mrb_value);

  add(dword [ecx + off0], 1);

  return mrb_true_value();
}

extern "C" mrb_value
mrbjit_prim_fix_succ(mrb_state *mrb, mrb_value proc)
{
  MRBJitCode *code = (MRBJitCode *)mrb->compile_info.code_base;

  return code->mrbjit_prim_fix_succ_impl(mrb, proc);
}

インライン化するコードはC++で書かれた実際にインライン化を行うMRBJitCodeのメソッド(MRBJitCode::mrbjit_prim_fix_succ_impl)と、そのメソッドを単に呼び出すCで書かれた関数(mrbjit_fix_succ)の組になります。C++で定義されたメソッドはProcオブジェクトとして扱えないからです。

インライン化するメソッドを見てみましょう。

mrb_value
MRBJitCode::mrbjit_prim_fix_succ_impl(mrb_state *mrb, mrb_value proc)
{
  mrbjit_vmstatus *status = mrb->vmstatus;
  mrb_code *pc = *status->pc;
  int i = *pc;
  int regno = GETARG_A(i);
  const Xbyak::uint32 off0 = regno * sizeof(mrb_value);

  add(dword [ecx + off0], 1);

  return mrb_true_value();
}

単にadd命令を生成するだけです。足すだけといってもどこを足すのか指定しなければならないので少し複雑です。オーバフロー時の処理を忘れていました。TODOということで・・・。

mrb->vmstatusにvmの内部状態が入っています。この中から現在実行中の命令(当然OP_SENDですね)を取り出します。これはpcメンバーにあります。
OP_SEND命令のA引数にはselfのレジスタ番号が入っているのでこれを取り出してmrb_valueのサイズを掛けてバイト単位のオフセットを求めています。

そして、add命令を生成してTrueを戻り値として返します。Non-Nilなのでこのインライン化は有効です。

今回は常にインライン化は有効になりますが、引数の条件によってインライン化は行わないようにすることも可能です。この場合はreturn mrb_nil_value();とします。

primitive.ccを見るともっと複雑な例をありますので、興味のある人は見てください。

そんなこんなで今回は終わり。そろそろねたも尽きかけたのでまた新しい機能ができたらお会いしましょう。ではでは

luajitの実力

追記
LuaJITの作者Mike Pall氏より、twitterで次のようなアドバイスをいただきました。

1. No compiler is allowed to make this optimization. Floating-point arithmetic ist NOT associative.
2. Please use 'local' functions when publishing Lua benchmarks.
3. Please use the current version of LuaJIT.

訳(かなり怪しい)

1.このような最適化出来るコンパイラは無いよ。浮動小数点数の算術命令は結合的じゃないから
2. Luaベンチマークを取るなら局所関数を使ってください
3. 最新バージョンのLuaJITを使ってください

そういうわけで、ベンチマークを取り直します。

ベンチマークをやり直しました。functionの前にlocalを入れて、LuaJITを最新にしています。Mike Pall氏には、ベンチマークのやり直しにあたりアドバイスをいただきました。ありがとうございます。

$ luajit -v
LuaJIT 2.0.1 -- Copyright (C) 2005-2013 Mike Pall. http://luajit.org/

最適化前

$ time luajit spline0.lua

real    0m1.275s
user    0m1.170s
sys     0m0.031s

最適化後

$ time luajit spline1.lua

real    0m0.806s
user    0m0.732s
sys     0m0.015s

最適化前が3%ほど速くなって最適化後との差が少し縮みました。JIT無しのLuaでの結果です。

$ time ./lua ../../luajit.org/spline0.lua

real    0m1.273s
user    0m1.185s
sys     0m0.045s

$ time ./lua ../../luajit.org/spline1.lua

real    0m0.327s
user    0m0.249s
sys     0m0.046s

追記終わり

それはあまりにもコンパイラの最適化に期待し過ぎです。実際に吐き出したコードを読んでみましょう。あなたがコンパイラの作者だったら、あなたがJITの作者だったら、入って来たコードから同じような最適化ができるでしょうか。まず無理です。どんな高度な最適化コンパイラも、所詮は人間の作ったコードです。コンパイラは神ではないのです。あくまでも人間の創りだした不完全な道具のひとつに過ぎません。

よくわかる最適化 (http://d.hatena.ne.jp/shi3z/20130502/1367490202) より

うう、luajitなら、luajitならやってくれる。と信じて確かめてみました。
結果、

最適化前

$ time luajit-2.0.0-beta10 spline0.lua

real    0m1.268s
user    0m1.200s
sys     0m0.016s

最適化後

$ time luajit-2.0.0-beta10 spline1.lua

real    0m0.795s
user    0m0.733s
sys     0m0.046s

理論値3倍のはずなのでかなり盛り返しています。

ちなみに、JIT無しのluaだとこんな感じ。
時間がかかってしょうがないのでループを1/100にしています。
最適化前

$ time ./lua ../../luajit.org/spline0.lua

real    0m1.309s
user    0m1.216s
sys     0m0.046s

最適化後

$ time ./lua ../../luajit.org/spline1.lua

real    0m0.331s
user    0m0.249s
sys     0m0.061s

ちゃんと、最適化は出来ているようです。

まとめ
 luajitはとても頑張っているが完璧ではない。

移植したソースコードです

最適化前

function catmullRom(p0, p1, p2, p3, t)
  local v0 = (p2 - p0) / 2.0
  local v1 = (p3 - p1) / 2.0
  return ((2.0 * p1 - 2.0 * p2) + v0 + v1) * t * t * t + 
          ((-3.0 * p1 + 3.0 * p2) - 2.0 * v0 - v1) * t * t + v0 * t + p1
end

function main(xp0, xp1, xp2, xp3, yp0, yp1, yp2, yp3, pp0, pp1, pp2, pp3)
  local d = math.sqrt((xp1 - xp2) * (xp1 - xp2) + (yp1 - yp2) * (yp1 - yp2))
  local num = math.ceil((d / 5.0) + 0.5)
  local x,y,p
  local invertNum = 1.0/num
  local deltaT = 0
  for i = 0, num do
    deltaT = deltaT + invertNum
    x = catmullRom(xp0,xp1,xp2,xp3, deltaT)
    y = catmullRom(yp0,yp1,yp2,yp3, deltaT)
    p = catmullRom(pp0,pp1,pp2,pp3, deltaT)
  end
end

for j = 0, 10000 do
  main(1.0, 100.0, 200.0, 200.0, 300.0, 100.0, 0.0, 200.0, 0.0, 100.0, 200.0, 300.0)
end

最適化後

function main(xp0, xp1, xp2, xp3, yp0, yp1, yp2, yp3, pp0, pp1, pp2, pp3)
local dx = xp1-xp2
local dy = yp1-yp2
local d = math.sqrt(dx*dx+dy*dy) 
local num = math.ceil((d*0.2) + 0.5)
local x,y,p
local invertNum = 1.0/num
local deltaT = 0
local xv0 = (xp2-xp0)*0.5
local xv1 = (xp3-xp1)*0.5
local xfact1=((xp1 - xp2)*2.0 + xv0 + xv1)
local xfact2=((xp2 - xp1)*3.0 - 2.0 * xv0 - xv1) 
local yv0 = (yp2-yp0)*0.5
local yv1 = (yp3-yp1)*0.5
local yfact1=((yp1 - yp2)*2.0 + yv0 + yv1)
local yfact2=((yp2 - yp1)*3.0 - 2.0 * yv0 - yv1) 
local pv0 = (pp2-pp0)*0.5
local pv1 = (pp3-pp1)*0.5
local pfact1=((pp1 - pp2)*2.0 + pv0 + pv1)
local pfact2=((pp2 - pp1)*3.0 - 2.0 * pv0 - pv1)
local xfact1n =0
local yfact1n =0
local pfact1n =0
local xFact1step = xfact1 * invertNum
local yFact1step = yfact1 * invertNum
local pFact1step = pfact1 * invertNum
  for i = 0, num do
     deltaT = deltaT + invertNum
     x =((xfact1n + xfact2) * deltaT + xv0) * deltaT + xp1
     y =((yfact1n + yfact2) * deltaT + yv0) * deltaT + yp1
     p =((pfact1n + pfact2) * deltaT + pv0) * deltaT + pp1
     xfact1n = xfact1n + xFact1step
     yfact1n = yfact1n + xFact1step
     pfact1n = pfact1n + xFact1step
  end
end

for j = 0, 1000000 do
  main(1.0, 100.0, 200.0, 200.0, 300.0, 100.0, 0.0, 200.0, 0.0, 100.0, 200.0, 300.0)
end

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

お久しぶりです。ここんとこしばらくProcオブジェクトのサポートを作りこんでました。これが無いとイテレータとかみんなVMに戻ってしまって性能が上がらないのです。実はProcオブジェクトをサポートしてもあまり性能が上がらなかったのですが…。
で、この作業ですごくとりにくいバグがいっぱい出て数カ月デバッグ三昧という感じでした。おかげてうまく動くようになると却って落ち着かないという状態なのですが、それはそれとしてそのデバッグで作ったツールを紹介したいと思います。全国に31名くらいいると思われるmrubyでJITコンパイラを作っている人たちに参考になれば幸いです。

デバッグしていて困るのはどの命令を実行していた時にバグったのかが分からないことです。vm.cで実行していた場合は命令毎に処理が分かれているのでまだいいのですが、ネイティブコードでバグった場合(例えばセグフォしたばあいとか)、mrubyのどの命令がコンパイルされたものでバグったのか分からないわけです。
幸い、mrubyのJITではVMに処理を渡すためにmrubyのVMのプログラムカウンタ(pc)と実行中のメソッドのirepをこまめに更新しています。これらを頼りに実行位置が付きとめられます。ところが、pcとirepがつじつまが合っていないという場合があって、この場合ほぼ確実にセグフォするのですがpcに対応するirepが分からないからいろいろ不便です。また、バイナリを見てmruby VMのどの命令か判断するのは結構出来るようになったのですが、とてもむなしい作業です。

そんなこんなで次のような関数を作ってデバッグ効率を上げました。

  • search_irep(mrb, pc)       pcに対応するirepを探す
  • disasm_irep(mrb, irep)      irepを逆アセンブルする
  • disasm_once(mrb, irep, 命令)   1命令を逆アセンブルする

実装は、https://github.com/miura1729/mruby/blob/95dc9d1c5596c96aae6a6814e98e09954f0c96f4/src/jit.cの398行目以降です。

こんな感じで使います

For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/miura/work/mruby/bin/mruby...done.

aoベンチを実行

(gdb) r benchmark/ao-render.rb
Starting program: /home/miura/work/mruby/bin/mruby benchmark/ao-render.rb
[New Thread 7272.0x2f00]
[New Thread 7272.0x3118]
P6
64 64
255

ちなみに今のバージョンはちゃんと動きます。説明用にバグを仕込ませています。

Program received signal SIGSEGV, Segmentation fault.
0x3d24418b in ?? ()

取り合えず、irep, pcがアクセスできそうな関数を探す。mrbjit_dispatchではpcはppcという変数名でpcへのポインタという形で持っている。

(gdb) where
#0  0x3d24418b in ?? ()
#1  0x00429041 in mrbjit_dispatch (status=0x22a8c0, mrb=0x20039920)
    at C:\cygwin\home\miura\work\mruby\src\vm.c:682
#2  mrb_run (mrb=0x20039920, proc=0x2003b798, self=...)
    at C:\cygwin\home\miura\work\mruby\src\vm.c:2325
#3  0x00419ecf in load_exec (mrb=0x20039920, p=0x200c01a0, c=<optimized out>) at src/parse.y:5206
#4  0x00427474 in mrb_load_file_cxt (mrb=0x20039920, f=0x200bff34, c=0x200c0148) at src/parse.y:5215
#5  0x004017ac in main (argc=2, argv=0x22ac40)
    at C:\cygwin\home\miura\work\mruby\tools\mruby\mruby.c:281

mrbjit_dispatchに対象フレームを移す

(gdb) up
#1  0x00429041 in mrbjit_dispatch (status=0x22a8c0, mrb=0x20039920)
    at C:\cygwin\home\miura\work\mruby\src\vm.c:682
682           asm volatile("call *%0\n\t"

pcに対応するirepを探す。

(gdb) p search_irep (mrb, *ppc)
$1 = (mrb_irep *) 0x200f3ec8

irepの命令列を逆アセンブルする

(gdb) p disasm_irep (mrb, $1)
   0 OP_ENTER   1:0:0:0:0:0:0
   1 OP_GETIV   R4      @x
   2 OP_MOVE    R5      R1
   3 OP_SEND    R5      :x      0
   4 OP_NOP
   5 OP_MUL     R4      :*      1
   6 OP_NOP
   7 OP_GETIV   R5      @y
   8 OP_MOVE    R6      R1
   9 OP_SEND    R6      :y      0
   a OP_NOP
   b OP_MUL     R5      :*      1
   c OP_NOP
   d OP_ADD     R4      :+      1
   e OP_NOP
   f OP_GETIV   R5      @z
  10 OP_MOVE    R6      R1
  11 OP_SEND    R6      :z      0
  12 OP_NOP
  13 OP_MUL     R5      :*      1
  14 OP_NOP
  15 OP_ADD     R4      :+      1
  16 OP_NOP
  17 OP_MOVE    R3      R4
  18 OP_RETURN  R3
$2 = void

どこを実行していたかはこんな感じで調べられる

(gdb) p *ppc - $1->iseq
$3 = 4

そんな感じです。またお会いしましょう。

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

今回からmrubyのJITで実際にどういうX86のコードを生成するのか説明する予定なのですが、その前に大変素晴らしいXbyakの宣伝とともにmrubyのJITで共通して使っているテクニックを紹介したいと思います。
Xbyakは光成滋生氏によって開発されたx86(IA32), x64(AMD64, x86-64)のマシン語命令を生成するC++のクラスライブラリです。詳しくは http://homepage1.nifty.com/herumi/soft/xbyak.html を読んでください。非常に使いやすく安定しているので開発が捗ることでしょう。
個人的に気になった点としては実行時のアセンブルエラーは例外をキャッチしないとどこで発生したかとかどういうエラーかが分からないことがあります。ただ、これはエラー時のハンドリングの柔軟性とのトレードオフなので難しいところです。

mrubyのJITで使っているテクニックを紹介します。

L("@@")は便利

 ドキュメント(http://homepage1.nifty.com/herumi/soft/xbyak.html)にあるようにXbyakMASM由来のL("@@")という無名のラベルが使えます。"@f"で前方の最寄りのL("@@")に"@b"で後方の最寄りのL("@@")を参照します。
mrubyのJITでは変数が想定したクラスのオブジェクトかどうかなどのチェックを行うガードを多用します。ガードはあちこちに似た形で存在しますのでユニークなラベルの名前を付けるのが困難です。こんな時はL("@@")が便利です。
例えば、eaxが指すオブジェクトが想定した型であるかどうかをチェックするガードを生成するgen_type_guardを見てみましょう。t

  void 
    gen_type_guard(mrb_state *mrb, enum mrb_vtype tt, mrb_code *pc)
  {
    /* Input eax for type tag */
    if (tt == MRB_TT_FLOAT) {
      cmp(eax, 0xfff00000);
      jb("@f");
    } 
    else {
      cmp(eax, 0xfff00000 | tt);
      jz("@f");
    }

    /* Guard fail exit code */
    gen_exit(pc, 1);

    L("@@");
  }

ttが想定した型で、mrb_vtypeはalue.hで定義されています。ここで見たとおりL("@@")が使われています。こうすることで何個ガードを生成しても問題が起こることはありません。

構造体のアクセス

mrubyのJITが生成した機械語コードはCで記述されたVMの状態を読み書きしながら実行するためCの構造体のメンバーにアクセスすることが必要です。機械語レベルで構造体のメンバーにアクセスすることはCコンパイラによって構造体のレイアウトが異なる場合があったりして煩雑なのですが、Xbyakでは構造体の先頭からのメンバーのオフセットを求めるOffsetOfとの合わせ技で比較的簡単に行うことが出来ます。
例えば、機械語コードからmrubyのVMに戻る処理を生成するgen_exitを見てみましょう。

  void 
    gen_exit(mrb_code *pc, int is_clr_rc)
  {
    const void* exit_ptr = getCurr();

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, pc)]);
    mov(dword [eax], (Xbyak::uint32)pc);
    if (is_clr_rc) {
      xor(eax, eax);
    }
    mov(edx, (Xbyak::uint32)exit_ptr);
    ret();
  }

gen_exitでは機械語コード実行に伴って古いpcはつじつまが合わないので更新する必要があります。そこで、pcの値()を引数で受け取って設定するようにしています。この時VMで使っているpcという変数のアドレスはstatus構造体に格納されているのでここからpcというメンバーにアクセスする必要があります。これが、次の部分です。
ebxにはstatus構造体の先頭アドレスが入っています。

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, pc)]);
    mov(dword [eax], (Xbyak::uint32)pc);

このように少し表記は煩雑ですがOffsetOfを使うことでコンパイラの違いを意識することなく構造体のメンバーにアクセスできます。

なお、OffsetOfはoffsetofという名前でC99であstddef.hで定義された標準のようですが、私の使っているCygwinではないので次のように定義して使っています。

#define OffsetOf(s_type, field) ((size_t) &((s_type *)0)->field) 

追記
 私の使っているCygwinにもstddef.hにありました。教えてくださった、egtraさん、herumiさんありがとうございます。

続く

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

かなり間が空いちゃいました。その間にmrubyのJITをコーティングしたりお祭りの資料を作ったりしていました。結構内容が変わって、例えばこれまで説明していたものがjit.cからvm.cに移ったりしています。そんなこんなで結構速度が上がって(多くがwannabe53さんのおかげ)、もうすぐオリジナルの2倍速くらいか(単純なループなら4倍くらいだけどメソッドコールが入ると速度が落ちる)というところまで来ています。

さて、今回はjitcode.ccの説明です。

const void *
mrbjit_emit_code(mrb_state *mrb, mrbjit_vmstatus *status)
{
  MRBJitCode *code = (MRBJitCode *)mrb->compile_info.code_base;
  const void *rc = mrbjit_emit_code_aux(mrb, status, code);
  if (rc == NULL && code == NULL) {
    mrb->compile_info.code_base = NULL;
  }

  return rc;
}

これが、コード生成部のトップレベルmrbjit_emit_codeです。最初に、

  MRBJitCode *code = (MRBJitCode *)mrb->compile_info.code_base;

でCレベルで持っているvoid *型のXbyakのCodeGeneratorオブジェクトをC++のオブジェクトに変換します。変換って言っても単にキャストですが。なんか、すごい勢いで怒られそうなコードですがとりあえず動いているからいいかって感じです。

  const void *rc = mrbjit_emit_code_aux(mrb, status, code);
  if (rc == NULL && code == NULL) {
    mrb->compile_info.code_base = NULL;
  }

ここで、実際の処理を行うmrbjit_emit_code_auxを呼び出します。mrbjit_emit_code_auxはコード生成をするとそのコードの命令列の先頭のアドレスを返します。コードが生成され合い場合はNULLを返します。
その後のif文はなぜいるのか忘れてしまった説明出来ないです。すみません・・・。これがないと動かないのですが…

次に、mrbjit_emit_code_auxです。

static const void *
mrbjit_emit_code_aux(mrb_state *mrb, mrbjit_vmstatus *status, MRBJitCode *code)
{
  mrb_irep *irep = *status->irep;
  mrb_value *regs = *status->regs;
  mrb_code **ppc = status->pc;
  const void *entry;

  if (code == NULL) {
    code = the_code;
    mrb->compile_info.code_base = code;
    entry = code->gen_entry(mrb, irep);
  }

初めてmrbjit_emit_codeを呼び出したとき、codeはNULLになっているのでCodeGeneratorオブジェクトを設定します。今のところ、CodeGeneratorオブジェクトは1つでthe_codeというグローバルのstatic変数に入っています。ただ、複数のCodeGeneratorオブジェクトを管理できるようにするため、the_codeを直接アクセスする箇所を出来る限り減らしています。code->gen_entry(mrb, irep)は今のところ何もしないのですが、CodeGenratorオブジェクトになんか設定する場合とかを想定しています。

  switch(GET_OPCODE(**ppc)) {
  case OP_NOP:
    return code->emit_nop(mrb, irep, ppc);
    
  case OP_MOVE:
    return code->emit_move(mrb, irep, ppc);

  case OP_LOADL:
    return code->emit_loadl(mrb, irep, ppc);

次に実行しようとする命令に対応するコードを生成します。emit_*というメソッドはjitcode.hに定義されていてほとんどはそれを呼び出すだけです。

例外は、OP_ENTERとOP_RETURNでそれぞれ次のようになっています。

  case OP_ENTER:
    mrb->compile_info.nest_level++;
    return code->emit_enter(mrb, status);

  case OP_RETURN:
    mrb->compile_info.nest_level--;
    if (mrb->compile_info.nest_level < 0) {
      return code->emit_return(mrb, status);
    }
    else {
      return code->emit_return_inline(mrb, status);
    }

OP_ENTERはメソッドの先頭、OP_RETURNはメソッドの最後の処理です。mrb->compile_info.nest_levelは現在のメソッドの呼び出しのネストレベルを表します。このレベルはネイティブコードで実行する場合で途中でVMに戻る場合は0に戻されます。また、メソッドがインライン化しない場合も0戻されます。つまり、OP_RETURNの時点でnest_levelが1以上の場合はOP_ENTERからOP_RETURNまでVMに戻らず、しかもインライン化するメソッドということになります。そういう場合は特別扱いして高速なコードを生成します。特別扱いするコード生成のメソッドが、emit_return_inlineです。

  default:
    mrb->compile_info.nest_level = 0;
    return NULL;
  }

普通この手のdefaultはnot reachedでエラーチェックのためにあるって感じですが、現時点のmrubyのJITは普通に実行されます。サポートされていない命令はVMJITで実行されます。ちなみに、emit_*においてもNULLを返すとその命令はVMで実行されます。例えば、OP_SENDで可変引数とかコード生成が超面倒な割には使用頻度が少ないのでNULLを返してVMで実行してもらっています。

そういうことで、今回は終わり
gdgdですがまあ仕方がない。次回はあるのか???

多分続く

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

祝その7、前回(ytljitの型推論)も前々回(yarv2llvm)も第6回くらいで挫折したんだよな。まああいつらよりは説明しやすいのですが。

そういうことで、無事にかは知らんけど拡張asmを超えて続きを説明します。ちなみに、今やっているのはjit.cのmrbjit_dispatchです。前回2回使って70行くらいしか説明していなんだな。どうでもいいことだけど。

ネイティブコードを実行して、無事戻ってきたところからです。

      irep = *status->irep;
      regs = *status->regs;
      n = ISEQ_OFFSET_OF(*ppc);
      if (irep->ilen < NO_INLINE_METHOD_LEN) {
	caller_pc = mrb->ci->pc;
      }
      else {
	caller_pc = NULL;
	mrb->compile_info.nest_level = 0;
      }
      if (rc) {
	mrb->compile_info.prev_pc = *ppc;
	return rc;
      }
      ci = search_codeinfo_prev(irep->jit_entry_tab + n, prev_pc, caller_pc);

ごちゃごちゃと後始末が続きます。
あれ、これって見たことがあると思った方、記憶力がいいですね。うらやましい。mrbjit_dispatchの先頭で出てきました。ローカル変数の初期化とcode_infoのサーチ処理です。ネイティブコードがirep, regsなどを書き換えているため、ローカル変数を再設定しているのです。

      if (rc) {
	mrb->compile_info.prev_pc = *ppc;
	return rc;
      }

これは出てないです。rcはネイティブコードから返される値の1つです。内容は、例外など特別に対処が必要なことがあるとそのハンドラのアドレスが返ってきます。ハンドラはmrb_runのgotoのラベルで、第3回(http://d.hatena.ne.jp/miura1729/20130211/1360607189)で説明した通り、mrbjit_dispatchの戻り値はその後mrb_run中でgotoされますからrcの値をそのまま返します。rcがNULLなら正常運転なので次に続きます。

ここからは実際にコンパイルするところです。ちなみにネイティブコードを呼んだ場合もここを通ります。

  if (irep->prof_info[n]++ > COMPILE_THRESHOLD) {
    //      printf("size %x %x %x\n", irep->jit_entry_tab[n].size, *ppc, prev_pc);
    if (ci == NULL) {
      //printf("p %x %x\n", *ppc, prev_pc);
      ci = add_codeinfo(mrb, irep->jit_entry_tab + n);
      ci->prev_pc = prev_pc;
      ci->caller_pc = caller_pc;
      ci->code_base = mrb->compile_info.code_base;
      ci->entry = NULL;
      ci->used = -1;
    }

    if (ci->used < 0) {
      entry = mrbjit_emit_code(mrb, status);
      if (prev_entry && entry) {
	//printf("patch %x %x \n", prev_entry, entry);
	cbase = mrb->compile_info.code_base;
	mrbjit_gen_jmp_patch(cbase, prev_entry, entry);
      }

      if (entry) {
	ci->entry = entry;
	ci->used = 1;
      }
      else {
	/* record contination patch entry */
	if (cbase) {
	  ci->entry = mrbjit_get_curr(cbase);
	}
	//	printf("set %x %x \n", ci->entry, entry);
	ci->used = -1;
	// printf("%x %x %x\n", ci->entry, *ppc, ci);
      }
    }
  }

コメントアウトされたprintfが痛いですね。

  if (irep->prof_info[n]++ > COMPILE_THRESHOLD) {

何回ここを通ったかをカウントします。現状ではCOMPILE_THRESHOLDの値は1000です。

    if (ci == NULL) {
      //printf("p %x %x\n", *ppc, prev_pc);
      ci = add_codeinfo(mrb, irep->jit_entry_tab + n);
      ci->prev_pc = prev_pc;
      ci->caller_pc = caller_pc;
      ci->code_base = mrb->compile_info.code_base;
      ci->entry = NULL;
      ci->used = -1;
    }

何度も出てきてます、既存のcode infoがあるかどうかのチェックです。無い場合は新たに作ります。usedが-1なのは確保されているけどネイティブコードは入っていないという意味です。

    if (ci->used < 0) {
      entry = mrbjit_emit_code(mrb, status);
      if (prev_entry && entry) {
	//printf("patch %x %x \n", prev_entry, entry);
	cbase = mrb->compile_info.code_base;
	mrbjit_gen_jmp_patch(cbase, prev_entry, entry);
      }

usedがさっそく出てきました。コンパイルは当然(でもないが)まだコンパイルしていない場所が対象です。

mrbjit_emit_codeはネイティブコード生成の心臓部です。今後説明する予定ですが、jitcode.ccで定義されているので興味のある人は見てみてください。少なくとも今説明しているmrbjit_dispatchよりは読みやすいと思います。mrbjit_emit_codeは生成したネイティブコードの先頭を返します。これをentryという変数に入れます。

     if (prev_entry && entry) {
	//printf("patch %x %x \n", prev_entry, entry);
	cbase = mrb->compile_info.code_base;
	mrbjit_gen_jmp_patch(cbase, prev_entry, entry);
      }

prev_entryとは何でしょう?これは、ネイティブコードからの戻り値の1つで終了処理の先頭アドレスが入っています。つまり、ネイティブコードが実行されていなければこの値はNULLです。
このことから、このif文は次のような条件の判定になります。

 何らかの理由でコンパイル出来ず一旦VMに戻るコードを生成してしまったけど、
再度挑戦したらコンパイルに成功して続けてネイティブコードで実行出来るようになった

その場合は、邪魔な終了処理をJMP命令に書き換えて(出た!)VMに戻らずコードを直通させるようにしています。

      if (entry) {
	ci->entry = entry;
	ci->used = 1;
      }
      else {
	/* record contination patch entry */
	if (cbase) {
	  ci->entry = mrbjit_get_curr(cbase);
	}
	//	printf("set %x %x \n", ci->entry, entry);
	ci->used = -1;
	// printf("%x %x %x\n", ci->entry, *ppc, ci);
      }

ネイティブコードが生成出来たらcode infoにそのエントリーを入れます。あとは、ネイティブコードが入っているよっていう印のusedに1を入れます。
else節って意味あったけかな?いろいろ試行錯誤した跡が残ってしまった感じだから良く分からない (ヲイ)

mrbjit_dispatchの終わりのところです。

  if (cbase && entry == NULL) {
    /* Finish compile */
    mrbjit_gen_exit(cbase, mrb, irep, ppc);
    mrb->compile_info.code_base = NULL;
    mrb->compile_info.nest_level = 0;
  }

if文はコンパイル中でネイティブコードが作られなかった場合という条件で、この場合はVMに戻る処理を生成して、もはやコンパイル中ではないということを表すためcode_baseをNULLにします。そして、nest_levelを0にしてこのメソッドの中でOP_RETURNが出てきてもコンパイルしないようにします。

  mrb->compile_info.prev_pc = *ppc;

prev_pcに現在のpcの値を入れておきます。つぎに、mrbjit_dispatchが呼ばれた時はmrb_runでpcが更新されて、prev_pcと*ppcは違う値になっているはずです。

  return status->optable[GET_OPCODE(**ppc)];

正常に実行された場合のとび先を求めてmrb_runに返します。

いやー面倒なところが終わった!次は緩くjitcode.ccに行きます。OP_SENDまでは楽出来そうだ。

続く♪