JIT の CALL インストラクションで手を抜きたい

VM ループの CALL 命令は

case (CALL):
{
    #include "長いコード"
}

のようになっている。include しているのは CALL の合成命令、例えば REFER_GLOBAL_CALL などで同じコードを使いたいから。
さて CALL 命令を JIT 化するには長いコードをアセンブリでごりごり書かないといけないんだけど、とても面倒で心が折れそう。さらに長いのでメンテナンスが大変そう。というわけで気が進まない。


それなら「長いコード」の部分を C++ の関数にしてしまえば JIT 化が楽だよねと思いつく。call すれば良いだけだから。
問題は CALL インストラクション毎に call/ret のコストが増えてしまう事。でもこれは実際に計測してみないと分からないのでやってみた。
ベンチマークに使ったのは

  • R6RS test suite
  • fib, triangle, clos

など。環境は X86-64 。引数は2つなのでレジスタ渡し。


結論としては triangle が 1 割ほど遅くなった以外は全く差がなかった。実践的なコードでは CALL インストラクションが発行される割合が少ないからだと思われる。これが例えばローカル変数参照の REFER_LOCAL とかだと結果が違ったかもしれない。

どうするか?

方針は 2 つある。

  1. CALL 命令を関数にまとめて VM ループ、JIT どちらもその関数を call する
  2. CALL 命令を関数にまとめて JIT はその関数を call する。VM ループは今まで通り。


前者はシンプル、後者は複雑さが残るが triangle のようなケースを救える可能性がある。ただ trianble も JIT コンパイルされる可能性を考えると微妙か?
とりあえず後者で進めてみて、以後の実装で計測し問題がなければ統一するってことにしよう。


と思ったけどよく考えたら C++ なのだから前者にしておいて VM ループでは必ず inline 展開されるように配慮すれば良い事に気付いた。すっきり。