コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。
いきなり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)の部分は以下のように実行されます。
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()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! |
|
INIT_DISPACTH*2, CASE, NEXTの定義はmrb_run()の少し上に書かれています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| - | | | ! |
|
gccかどうかで定義が変ってます。めんどくさいけどちゃんとマクロ展開されたコードを示すことにします。
gccじゃない場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! ! |
|
gccの場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! |
|
というわけでgccじゃない場合は無限ループ & switch文ですが、gccの場合は命令コードで決まるジャンプ先に直接飛んでいます。こうすることで命令ごとに条件分岐をするコストがなくなるため高速化が実現できます。YARVでも同じことが行われていました。
総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。
(執筆中)