Rubyはメソッドの再定義が出来るのですが、これがJITコンパイラを作るときに問題になります。メソッドそのものを再定義しなければならないのはもちろん、各所に散らばっているそのメソッドを呼び出している元も書き換えなければならないからです。さもなければ、再定義されているかを毎回チェックする必要があり、大幅に速度が落ちます。
mrubyのJITではコードの自己書き換えを駆使することで速度を落とさず再定義に対処しています。ただし、メモリ効率が悪いので、頻繁な書き換えには向きません。
mrubyのJITではこんな感じで生成コードを管理しています。この場合は、メソッドfooの場合です。
entry tableってのがあって、RITE VMの命令毎に対応する機械語コードのアドレスが入っています。Tracing JITなのでプログラムが全て機械語になっている保証はないですので、これを見て対応する機械語命令があればそれにジャンプするっていう仕組みになっています。対応する機械語が無い場合は0が入ります。
さてメソッドfooを再定義してみましょう。この場合、すでに各所にfooの呼び出しが散らばっていてこのままでは古いコードが引き続き使われてしまいます。
ちなみにmrubyのJITではcall命令ではなくjmp命令を使ってメソッド呼び出しをします。戻り先はcallinfoに格納します。
再定義時に前に定義された命令は、次のように書きかえられます。
- entry tableをすべて0クリアする
- 生成コードにパッチを当てる。パッチの内容はreset_callerを呼び出して、VMに戻るというものです。
reset_callerはこの無効化したメソッドを呼んだブロックやメソッドの生成された機械語も無効化する関数です。callinfoを参照すればcaller のirepが取れるのでこういう芸当が可能です。
reset_caller(mrb_reset_caller)の定義はここにあります。
mrb_reset_callerはランチャーでレジスタに入ってるmrb_state構造体(esi)とpc(ebx)を取り出してmrb_reset_caller_auxに引数として渡すようなことをしています。実際の仕事はmrb_reset_caller_auxが行います。
このようにパッチを当てられた元のメソッドを呼び出すと、呼び出し元がreset_callerによってクリアされてしまいます。
きれいに消されてしまいました。これでこのブロックからは元の機械語が呼ばれることはありません。実行頻度が高ければすぐに新しいメソッドの定義の機械語が生成されることでしょう。
どれだけ沢山の場所から呼ばれていても同様にクリアされますので問題ありません。