[[mrubyを読む]] #contents *はじめに [#bdd53068] コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。 *mrubyVM概観 [#p63559f8] いきなりmrb_run()に入る前にmrubyVM((Riteはコードネームだから今後はmrubyと呼んで、とまつもとさんがつぶやいてたのでRiteVMと呼ばずにmrubyVMと呼びます))がどんな実行モデルなのかについて説明します。 mrubyVMの実行モデルはレジスタマシンです。ちなみにYARVはスタックマシンです。レジスタマシンとスタックマシンの違いはWikipediaあたりをご参照ください。 例えば以下の単純なRubyスクリプトの場合、 def foo(a, b) a * b end f = foo(1, 2) irep 116 nregs=6 nlocals=3 pools=0 syms=1 000 OP_TCLASS R3 001 OP_LAMBDA R4 I(117) 1 002 OP_METHOD R3 'foo' 003 OP_LOADSELF R3 004 OP_LOADI R4 1 005 OP_LOADI R5 2 006 OP_LOADNIL R6 007 OP_SEND R3 'foo' 2 008 OP_MOVE R1 R3 009 OP_STOP irep 117 nregs=7 nlocals=5 pools=0 syms=1 000 OP_ENTER 2:0:0:0:0:0:0 001 OP_MOVE R5 R1 002 OP_MOVE R6 R2 003 OP_LOADNIL R7 004 OP_SEND R5 '*' 1 005 OP_RETURN R5 f = foo(1, 2)の部分は以下のように実行されます。 +レジスタR3にselfをロード(レシーバを設定) +レジスタR4に1をロード(引数を設定) +レジスタR5に2をロード(引数を設定) +レジスタR6にnilをロード(ブロック引数を設定) +レジスタR3のオブジェクトに対して引数が2つである'foo'メソッドを実行(R(3+1)番目以降のレジスタの値がメソッド引数として使われます。詳しくは後で解説します) +レジスタR1(ローカル変数f)にメソッド呼び出しの結果(R3に格納されます)を設定 mrubyではレジスタの確保場所としてスタックを使用しています。そのため、mrubyVMはスタックマシンであると勘違いしてしまう危険があるので注意してください。さわだもソースだけ見ていてスタックマシンだと勘違いしていました。 レジスタの確保場所としてスタックを使うとはどういうことかというと、以下のようなイメージです。(OP_SEND '*'実行直前の状態) トップレベル実行時のスタックベース→| nil |top_self | |ローカル変数fの格納領域 | nil |よくわからない。特殊変数用? メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ | 1 |'foo'の引数1 & ローカル変数a | 2 |'foo'の引数2 & ローカル変数b | nil |'foo'に対するブロック引数 | nil |よくわからない。特殊変数用? | 1 |'*'のレシーバ | 2 |'*'の引数1 | nil |'*'に対するブロック引数 以上の前提を持ってmrb_run()に挑むと理解が深まると思います。 *mrb_run(src/vm.c) [#s87bb89e] では、mrb_run()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。 #code(C){{ INIT_DISPACTH { CASE(OP_NOP) { /* do nothing */ NEXT; } CASE(OP_MOVE) { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; NEXT; } ... } END_DISPACTH; }} INIT_DISPACTH((スペルミスだと思うのだけど、何故修正されないのだろう?)), CASE, NEXTの定義はmrb_run()の少し上に書かれています。 #code(C){{ #ifdef __GNUC__ #define DIRECT_THREADED #endif #ifndef DIRECT_THREADED #define INIT_DISPACTH for (;;) { i = *pc; switch (GET_OPCODE(i)) { #define CASE(op) case op: #define NEXT mrb->arena_idx = ai; pc++; break #define JUMP break #define END_DISPACTH } } #else #define INIT_DISPACTH JUMP; return mrb_nil_value(); #define CASE(op) L_ ## op: #define NEXT mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)] #define JUMP i=*pc; goto *optable[GET_OPCODE(i)] #define END_DISPACTH #endif }} gccかどうかで定義が変ってます。めんどくさいけどちゃんとマクロ展開されたコードを示すことにします。 gccじゃない場合 #code(C){{ for (;;) { i = *pc; switch (GET_OPCODE(i)) { { case OP_NOP: { /* do nothing */ mrb->arena_idx = ai; pc++; break; } case OP_MOVE: { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; mrb->arena_idx = ai; pc++; break; } ... } } }; }} gccの場合 #code(C){{ i=*pc; goto *optable[GET_OPCODE(i)]; return mrb_nil_value(); { L_OP_NOP: { /* do nothing */ mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]; } L_OP_MOVE: { /* A B R(A) := R(B) */ int a = GETARG_A(i); int b = GETARG_B(i); regs[a].tt = regs[b].tt; regs[a].value = regs[b].value; mrb->arena_idx = ai; i=*++pc; goto *optable[GET_OPCODE(i)]; } ... } ; }} というわけでgccじゃない場合は無限ループ & switch文ですが、gccの場合は命令コードで決まるジャンプ先に直接飛んでいます。こうすることで命令ごとに条件分岐をするコストがなくなるため高速化が実現できます。YARVでも同じことが行われていました。 *実行してみる [#n89a9da1] 総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。 (執筆中)