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のラップ並みに感謝