Tociyuki::Diary RSSフィード

tociyuki によるPerl・Rubyのコード説明を中心に、日常雑記も混在 : B  F  twitter  GitHub  CPAN  本館

2012年05月02日

[][]mrb_closure_new のバグを pull request

mruby で素数を数えるんだ - Tociyuki::Diary」で、引数がないときブロックからクロージャを作ったらうまく動かない原因を探すため、mruby のクロージャが生成されるまでを VM になったつもりで追いかけていました。

ブロックをコンパイルすると、OP_LAMBDA に irep 番号に OP_L_CAPTURE フラグ(値は2)を加えたコードが生成されます。下の場合ですと、 irep1 の 008 ステップがクロージャの生成指示部分です。

def prime_generator

irep 1 nregs=7 nlocals=5 pools=0 syms=2
...
007 OP_LOADSELF	R5
008 OP_LAMBDA	R6	I(2)	2   # irep(2) OP_L_CAPTURE
009 OP_SEND	R5	'lambda'	0
010 OP_RETURN	R5

  lambda do

irep 2 nregs=4 nlocals=2 pools=0 syms=3
000 OP_GETUPVAR	R2	2	0     # n += 1
001 OP_LOADI	R3	1
002 OP_LOADNIL	R4
003 OP_ADD	R2	'+'	2
004 OP_SETUPVAR	R2	2	0
...

VM の OP_LAMBDA の処理部分が読んでみると、OP_L_CAPTURE がついているときは、mrb_closure_new で処理をしています。mrb_closure_new の第二引数には、ブロック手続きが渡されます。上の場合ですと irep 2 を渡すわけです。

⇒ mruby/src/vm.c

1403     CASE(OP_LAMBDA) {
1404       /* A b c  R(A) := lambda(SEQ[b],c) (b:c = 14:2) */
1405       struct RProc *p;
1406       int c = GETARG_c(i);
1407 
1408       if (c & OP_L_CAPTURE) {
1409         p = mrb_closure_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]);
1410       }
1411       else {
1412         p = mrb_proc_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]);
1413       }
1414       if (c & OP_L_STRICT) p->flags |= MRB_PROC_STRICT;
1415       regs[GETARG_A(i)] = mrb_obj_value(p);
1416       NEXT;
1417     }

mrb_closure_new は proc.c に定義があります。

⇒ mruby/src/proc.c

  26 struct RProc *
  27 mrb_closure_new(mrb_state *mrb, mrb_irep *irep)
  28 {
  29   struct RProc *p = mrb_proc_new(mrb, irep);
  30   struct REnv *e;
  31 
  32   if (!mrb->ci->env) {
  33     e = mrb_obj_alloc(mrb, MRB_TT_ENV, (struct RClass *) mrb->ci->proc->env);
  34     e->flags= (unsigned int)irep->nlocals;
  35     e->mid = mrb->ci->mid;
  36     e->cioff = mrb->ci - mrb->cibase;
  37     e->stack = mrb->stack;
  38     mrb->ci->env = e;
  39   }
  40   else {
  41     e = mrb->ci->env;
  42   }
  43   p->env = e;
  44   return p;
  45 }

29行で生成した手続きオブジェクト p の env メンバを、33 行で生成した MRB_TT_ENV のタグがついたオブジェクトへ、43行で束縛しています。この env メンバを束縛しているオブジェクトがクロージャに結びついた環境になります。ですが、その中には、この段階ではまだ何もとりこまれていません。その代わりに、現在の環境の mrb->ci->env メンバを環境オブジェクトに束縛しています。

コードを追っていくと、生成した環境オブジェクトへの書き出しは OP_RETURN がおこなっています。

⇒ mruby/src/vm.c

 945     L_RETURN:
 946       if (mrb->ci->env) {
 947         struct REnv *e = mrb->ci->env;
 948         int len = (int)e->flags;
 949         mrb_value *p = mrb_malloc(mrb, sizeof(mrb_value)*len);
 950 
 951         e->cioff = -1;
 952         memcpy(p, e->stack, sizeof(mrb_value)*len);
 953         e->stack = p;
 954       }

ここが書き出し部分です。なのですけど、不思議に感じるのは、952 行でスタックから書き出されるオブジェクト数は、このスタック中のローカル変数の個数ではなくて、proc.c の 34 行でセットしたブロックのローカル変数の個数になっていることです。この時点ではブロックはまだ実行されていないので当然スタック上にはブロックのローカル変数はありません。

当初は、コンパイル時にブロックの irep の nlocals はクロージャを生成する環境のローカル変数の個数をセットするのかと勘違いしてコード生成部のおかしな部分を探していたのですが、コンパイル結果を眺めていると、ブロック自身のローカル変数の個数が常に正しく nlocals に反映していることがだんだんわかってきました。つまり、コード生成部は正常なのだろうと判断せざるをえませんでした。

となると、おかしいのは、proc.c の 34 行ということになります。ここはクロージャにしたいブロックの nlocals ではなくて、現在スタックを使用中 irep の nlocals でなければなりません。うまくいかないのは、クロージャを作るメソッドのローカル変数の個数が、ブロックのローカル変数の個数よりも多い場合です。そのようなソースを書いて動かしてみると、セグメンテーションエラーが生じることも確認できました。では、それはどうやって取得できるのかだろうかと、vm.c を眺めていたら、OP_SEND のところに次のようなコードが入っていることに気がつきました。

⇒ mruby/src/vm.c

 681         /* setup environment for calling method */
 682         proc = mrb->ci->proc = m;
 683         irep = m->body.irep;

これが意味するところは、現在実行中の irep は mrb->ci->proc->body.irep で求めることができるということです。試しに、gdb で proc.c の 34 行にブレークポイントを置いて、このやりかたで nlocals を取得できるのかどうかチェックしてみたら大丈夫でした。これで、現在実行中の手続きの nlocals を取得する方法がわかりましたので、proc.c を書き直してみました。

#if 0
    e->flags= (unsigned int)irep->nlocals;
#else
    e->flags= (unsigned int)mrb->ci->proc->body.irep->nlocals;
#endif

動かなかった引数なしブロックからクロージャを作る場合も、この修正で動くようになりました。ということで、問題は当初考えていた「ひょっとして、ゼロ個の引数のときはうまくいかないのかもしれない」というのは勘違いで、もっと根本的なところがおかしかったという結論に辿り着きました。

懸念事項として、環境を書き出される proc の body が VM コードなら、これでいいのですが、クロージャへ環境を書き出される元が C 言語で記述されたメソッドの場合にどうなるのだろうかと疑問は残ります。そこは実装の仕様にかかわるところで、預かりしれないため、とりあえず、上の修正で pull request しておきました。

5月4日に mruby/mruby master に上のパッチがマージされました。私の懸念事項は考え過ぎだったのかもしれません。

2012年04月23日

[]Reit VM 読み

mruby のインタープリタ Reit VM をじっくりと読んでいる最中です。

Riet VM のオブジェクト・コードの逆アセンブルリストは、bin/mruby に verbose オプションをつけると標準出力にパーサ出力の構文木出力に続いて表示されます。既存のオブジェクト・ファイルの逆アセンブルリストも、それで表示できます。最適化処理がないため、構文木からどのようにコード生成するのか、出力眺めるだけで手にとるようにわかりやすいです。

$ bin/mruby --verbose prime.rb

構文木は LISP の cons を使った連結リストになっていて、car、cdr がそのままソース中に記述されているので、LISP 慣れしている自分にとってはすごく src/codegen.c が読みやすくなっててうれしいことです。できれば、verbose の出力も括弧リストで表示してくれたら、Emacs の lisp-mode で追いかけやすくなったのにと残念でした。

コード読みには、GNU global の htags(1) で HTML を生成したものを使ってます。git リポジトリにタグファイルを作るのは避けて、アーカイブで /tmp にエクスポートし、そこでタグデータベースを作って HTML に変換しました。

$ cd mruby
$ mkdir /tmp/mruby
$ git archive --format=tar HEAD | (cd /tmp/mruby; tar xf -)
$ pushd /tmp/mruby
$ gtags
$ htags -sanoTt 'mruby'
$ mv HTML mruby
$ tar cf - mruby | (cd $HOME/www; tar xf -)
$ cd ..
$ rm -fr mruby
$ popd
$ chromium $HOME/www/mruby/index.html

以下、読みながらの雑感です。読み取りミスで嘘を書いている可能性が高いので、その辺、斟酌してください。

  1. どのような命令を持っているかは src/opcode.h を見るのが一番です。どのように実行するかは src/vm.c を読む。
  2. 単純に比較できるものではないのですけど、Lua VM が Lua-2.5.0 で 864 行なのに対して、ReitVM は 1544 行。
  3. VM インスタンスlua_State * に対して mrb_state *。これはオマージュというものなのか……。
  4. Ruby のソースはパーサで構文木に変換してから、そのまま最適化処理なしで、ReitVM のバイナリへ変換してます。出力されるコードを眺めるだけで手に取るように変換の様子がわかります。
  5. Reit VM は Lua VM っぽい 32 ビットの 3 オペランド命令であり、1 オペランドのスタック演算でもスタックポインタの位置を事前に解決しておいてフレーム・ディスプレースメントで直接アクセスする方式になっていて、今風です。
  6. Lua VM はレジスタアーキテクチャに対して、Reit VM は Smalltalk VM 流のフレーム・アーキテクチャです。Smalltalk VM はメソッドコールごとにオブジェクト・スペースにフレーム・インスタンスを作ってダイナミック・リンクするのですが、Reit VM はスタックは一本で、スタック・ベースを移動していきます。ここは ruby-1.8.7 以前と同じ。YARV はどうなっていたのでしたっけ?
  7. コルーチン (ファイバ・ジェネレータ) のかけらはまったくなし。
  8. デバッガ用の trace フック・ポイントはないようです。
  9. GC は、Lua VM と同じく、ダイクストラの3色インクリメンタル・マーク&スイープ。ライト・バリアもダイクストラのアルゴリズムをそのまま実装してます。オブジェクト構造体とオブジェクト・スペースの構造がシンプルな分、Reit VM の方が読みやすいです。
  10. Lua VM 同様に TAILCALL オペコードをもってます。Reit VM では callinfo ごと一緒にフレームのダイナミック・リンクを外してつけかえているようです。クロージャを作った場合はフレームはクロージャから参照されているので、GC の対象にはならないという点は Smalltalk VM と同じです。
  11. オブジェクト構造体 (mrb_value) は値と型の組の単純なタプルになっており、タグ付き短縮オブジェクトはなくなってます。 おかげで、GC のコードもすっきり綺麗に。
  12. クロージャ呼び出しの扱い方は一見 Lua VM に似ていますが (Upvar の名称とかに名残りがある)、Reit VM はフレームからフレームへと (callinfo経由で) リンクしていて、中身は別物です。
  13. VM から呼び出されるメソッド定義コードからは、src/class.c で定義されている mrb_get_args で、scanf(3) 感覚で VM の実引数を取得できます。これ、すごく便利ですし、読みやすくなってます。Perl XS とは別のアプローチで、mruby のやりかたの方が私は気に入りました。

mrb_get_args の書式

書式記号ポインタ意味
omrb_value *オブジェクト参照
imrb_int *整数 int32_t
fmrb_float *浮動小数点数 double
schar**, size_t*文字列
amrb_value **配列
bmrb_value*引数渡しのProcインスタンス
&mrb_value*ブロック、Ruby の &proc相当
*mrb_value**, size_t*Ruby の *args相当