mrubyを読む

はじめに

コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。

mrubyVM概観

いきなりmrb_run()に入る前にmrubyVM*1がどんな実行モデルなのかについて説明します。

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)の部分は以下のように実行されます。

  1. レジスタR3にselfをロード(レシーバを設定)
  2. レジスタR4に1をロード(引数を設定)
  3. レジスタR5に2をロード(引数を設定)
  4. レジスタR6にnilをロード(ブロック引数を設定)
  5. レジスタR3のオブジェクトに対して引数が2つである'foo'メソッドを実行(R(3+1)番目以降のレジスタの値がメソッド引数として使われます。詳しくは後で解説します)
  6. レジスタ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)

では、mrb_run()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
 
  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*2, CASE, NEXTの定義はmrb_run()の少し上に書かれています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 
 
 
 
 
-
|
|
|
!
 
 
 
 
 
 
 
 
 
 
 #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じゃない場合

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
!
  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の場合

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
-
|
|
!
|
-
|
|
|
|
|
|
|
!
|
|
!
 
  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でも同じことが行われていました。

実行してみる

総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。

(執筆中)


*1 Riteはコードネームだから今後はmrubyと呼んで、とまつもとさんがつぶやいてたのでRiteVMと呼ばずにmrubyVMと呼びます
*2 スペルミスだと思うのだけど、何故修正されないのだろう?

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS