mruby

mrubyのバイトコードの命令の解説

More than 1 year has passed since last update.

mrubyでコンパイラを作ってみたり、Rubyを書く以外の方法でmrubyのバイトコード列を書く場合、バイトコードの命令を知る必要があります。
バイトコード命令は単純そうで意外と奥が深いようです。ここではmrubyのJITを作成する経験で得たバイトコードの裏仕様を解説したいと思います。

命令の解説

OP_NOP

何もしない命令
これって実はcodegen.cで定義されている正規のコードジェネレータでは出てこないんですよね。でもバイトコードのパッチとかやりたいときはないと困る重要な命令です。
命令の仕様ではオペランドはないのですが、実際にはオペランドの領域があるのでなにか隠しデータを保存しておくのにも便利です。

OP_MOVE

MOVE Rm, RnでレジスタRnの内容をRmに代入する命令。n, mはレジスタの番号
こんなにいらんだろ?って思うほどいっぱい生成されます。OP_SENDで解説しますが引数はレジスタが連続していないといけないので多少はね。
ところで、MOVE命令は2種類あることに気づいていたでしょうか?見分け方はmとnの大小関係

  • m > nのとき、これはメソッドを呼び出すために連続したレジスタを用意するための命令です。

  • m < nのとき、これはレジスタRmはローカル変数が割り当てれていて、そのローカル変数の内容を更新する命令です

m = n の場合はcodegen.cのオプティマイザで省かれるはずです。
この2つの違いを意識すると、いろいろ効率の良いコンパイラが書けるかもしれませんね。
いずれにしても釣りはフナに始まりフナに終わる的な命令。実装は1行だけどね。

OP_LOADL

irepのPOOL領域からデータを引っ張ってきて、レジスタに入れる命令
現時点ではデータとして使えるのは、String, Float そしてなぜかFixnum。絶対値が大きな値はLOADIが使えないのであった。配列とかでも使えるようにしてアロケーションをさぼれるといいなとかおそらく3億人くらいが考えているだろうけど、irepはシリアライズしてファイルとかに書きだせることを忘れていてはいけない。YARVのputobjectとは違うのだよ。

OP_LOADI

Fixnumの値をレジスタに入れる命令
命令長が32bitなので当然31bitとかそういう大きい数は使えない。

OP_LOADSYM

irepのシンボル領域(syms)からシンボルを引っ張ってきて、レジスタに入れる命令
Rubyはメソッド名とかクラス名とかみんなシンボルとして扱うから結構出てくる。

OP_LOADSELF

現在のselfをレジスタに代入する命令
MOVE Rm, R0じゃ駄目なのか?と思わないでもないが、何か理由があるのだろう。私は知らないが。あ、mrubyのVMでは常にR0にselfが入っているんだよ。Rubyレベルでは何の役にも立たない知識でした。
LOADSELFはMOVEより少し早い可能性があるって言う利点はありそう。早いと言ってもほんの少しだけど、LOADSELFはいっぱい出てくるので効果が出てくるかもしれない。
LOADSELFでレジスタに入る内容は、メソッド内なら常に同じという性質は何かに使えるかもしれない。

OP_LOADT

OP_LOADF

レジスタにTRUE, FALSEの値を入れる
まあ・・・

OP_GETGLOBAL

グローバル変数の内容をレジスタに入れる
グローバル変数を指定するのにグローバル変数のシンボルを使うんですよ。つまり、毎回シンボルをキーにグローバル変数の検索をしているわけなんですよ。いかに、グローバル変数が冷遇されているかわかろうというものですよね。もっとも、インスタンス変数もシンボルで指定なんですが。

OP_SETGLOBAL

レジスタの内容をグローバル変数に入れる
あとはGETGLOBALと同じ

OP_GETSPECIAL

OP_SETSPECIAL

これって、GETGLOBAL, SETGLOBALとどう違うんだろう?って思うのが、人間として自然な感情だと思うけど、実はこの2つの命令は今は使われていないのでした。めでたしめでたし。

OP_GETIV

selfのインスタンス変数の内容をレジスタに入れる
インスタンス変数の指定はシンボルだ。つまり、グローバル変数と同じで毎回変数の検索が必要ってわけだ。
グローバル変数と違って使用頻度が多いからキャッシュとか駆使して速くするとよさそうだ。mrb->c->ci->target_classとR0が同じ時は静的にインスタンス変数のスロット位置が決まるから高速化できそう。みんな大好きao-benchではこれでかなり速度が上がるぞ。でも、CRubyでインスタンス変数アクセスの高速化の必要性を訴えて高速化を入れてもらったんだけど、全体としてはあまりうれしいことなかったらみたい。しょぼーん

OP_SETIV

レジスタの内容をselfのインスタンス変数に入れる

OP_GETCV

クラス変数の内容をレジスタに入れる
クラス変数なんてグローバル変数以上につかないよね。使ったとしてもなんかメタレベルプログラミングのカウンタみたいな使い方でそれほど使用頻度はなさそう。でも、mrubyのJITではこの命令はネイティブにコンパイルされる。なぜなら、ao-benchで乱数発生で使っているから。つまり、ao-benchは正義

OP_SETCV

レジスタの内容をクラス変数に入れる
これもmrubyのJITはサポートしている。理由は省略

OP_GETCONST

selfで定義されている定数をレジスタに入れる
定数なんだからコンパイル時に展開しておけばいいじゃんって思うだろ?Rubyは定数といっても書き換えれれるからね。まあ、みんな知っていることだけど。

OP_SETCONST

レジスタの内容でselfで定義されている定数を書きかえる
定数が書き換えれれるなら、定まって無いじゃん、って多分Matzさんは聞きあきてると思うよ

OP_GETMCNST

引数で指定したモジュールで定義されている定数をレジスタに入れる
OP_GETMCONSTじゃないことに注意。他のモジュールの定数をアクセスする命令があるなら、他のインスタンスのインスタンス変数をアクセスする変数も作ってよ、と個人的には思うけど、Foo::BAR みたいなコードをコンパイルするにはこれが無いとどうにもならない。一方、インスタンス変数ではアクセサが少し速くなるだけ。

OP_SETMCNST

レジスタの内容で、指定したモジュールで定義されている定数を書きかえる

OP_GETUPVAR

メソッドの呼び出し元のレジスタの内容を得る
複雑、できることなら関わりたくない。渋谷で女子高生に「mrubyのバイトコードの中で一番お付き合いしたくない命令はなんですか?」と聞けばきっとみんなGETUPVARというでしょう。たとえば、GETUPVAR R1, 0, 0とすると、R1に何が入る?ときいて即答するのはなかなか大変(答えは現在実行中のメソッドのself)。でも気にすることないよ、GETUPVARの命令の複雑さなんて、それを実行するためのクロージャが持っている環境の取り扱い周りにくらべれば大したことなくて、GETUPVARの誤使用以前にもっとめんどくさいバグで悩むだろうから。

OP_SETUPVAR

メソッドの呼び出し元のレジスタに書き込む

OP_JMP

プログラムカウンタを動かす(ジャンプ)
よくあるジャンプ命令。ハンドアセンブルを行う人にはつらい相対ジャンプですよ。-0x8000~0x7fffまでサポートしているから相当でっかいメソッド作っても平気だよ。でも、エラーチェックはしていないみたい。3万行くらいのif文作ると多分バグる。レッツトライ!

OP_JMPIF

引数のレジスタがnilかfalseではない時にジャンプする
この命令ってif文では生成されないんだぜ。とってもどうでもいい知識でした。ちなみにwhile文とかrescueとかorとかで生成されます。

OP_JMPNOT

引数のレジスタがnilかfalseの時ジャンプする。
if文ではこっちを使う。

OP_ONERR

現在のプログラムカウンタから引数だけ足した位置をmrb->c->rescueにしまう。
なんじゃこれ?と思いましたが(私が)、良く調べたら何でもないものでした。
begin ... rescue ... end で例外が発生した時にrescueに飛んでいく位置が必要になるわけですが、この命令でそれを記録するわけです。
いやー、この記事書いて1つかしこくなりました。JPOPのラップ並みに感謝