mrubyでメソッドをインライン化を実現するためのmrbgemです。
ソースはここにあります。
https://github.com/miura1729/mruby-inline
効果は、速くなったり、遅くなったり、場合によります。でも、将来的にはメソッドのインライン化は避けて通れない最適化だと思っています。
はっきり言ってmruby-inlineは単独では無意味ですが、色々知見を得るのに有意義だと考えています。
#使い方
インライン化したいメソッドがあるクラスにInlineモジュールをincludeします。
クラス定義のメソッド定義後に、インライン化したいメソッドに対して、
make_inline_method :メソッド名
とします。これで、そのメソッドを呼び出すときにインライン化します。
たとえば、こんな感じのコードになります。
class Vec
include Inline
def initialize(x, y, z)
@x = x
@y = y
@z = z
end
def vlength
Math.sqrt(@x * @x + @y * @y + @z * @z)
end
make_inline_method :vlength
end
#仕組み
このmrbgemの仕組みを簡単に説明します。
make_inline_methodで指定されたメソッドは、
- メソッドの実体(Procオブジェクト)がメソッド名をキーにしたハッシュ(inline_method_list)に格納されます
- メソッドそのものはundefされ未定義になります
そのメソッドが呼ばれた場合、未定義になっていますので、当然method_missingが呼ばれます。method_missingメソッドでは、
- __inline_method_list__からメソッド実体のProcオブジェクトを取り出し
- そのProcオブジェクトからirep(callee_irep)を取り出します。
- 呼び出し元のirep(caller_irep)をcallinfoから取り出します。
- caller_irepのiseqをcallee_irepが足せるくらい大きく拡張した後で、callee_irepを後にくっつけます。この際、レジスタの番号とかをインライン化に合わせて変えます。この辺のパッチの当て方は章を代えて説明します。
- 最後にこのメソッドを呼び出したsend命令をiseqの後ろにくっつけた命令列にjumpするようにパッチを当てます。
命令のパッチ当て
当然のことながらそのままインライン化したい命令列をくっつけただけではうまくいきません。メソッドはR0にselfが入り、続いて引数・ブロック・ローカル変数・作業レジスタと続いています。
しかし、メソッド呼び出しのお膳立てをする前は当然そうなっていません。ただ、規則性があり、R0がOP_SENDのAオペコードで示されるレジスタ番号(vm.cではaまたはaccで参照されます)になっています。そこで、regsの値をいじる代わりにR0をRaにR1をRa+1に書き換えるわけです。
書き換えるのは簡単ですが、命令によってオペランドのどこがレジスタか不規則です。そのため、命令のオペランドの型を示すデータベースを配列で持つことにしました。その配列が、opinfo.h( https://github.com/miura1729/mruby-inline/blob/master/src/opinfo.h )で定義されています。
大まかにはこれでいいのですが、これでは駄目な場合があります。命令毎に細かくパッチを当てていきます。たとえば、こんな命令です。
-
OP_RETURN
OP_SENDで呼び出したわけではないので、OP_RETURNを呼び出すとおかしくなります。これはJMP命令に書き換えます。 -
OP_ENTER
この命令はOP_SENDでおぜん立てされた新鮮なフレームじゃないと動かない命令です。大部分はNOPに変えても大丈夫です。大丈夫じゃない時はインライン化はあきらめてください。