Gauche:VMの最適化:JIT
昔のやつ:
(Shiro:2023/12/27 09:15:12 UTC)
JITというか、一部をネイティブコードにする実験をやってたんだけど、現行のVM+ネイティブコードをSUBRとして呼び出し、では結局ほとんどこれ以上速くならない、という結論に。
最大の問題は、途中にネイティブコード呼び出しが挟まっても継続がうまく動くようにすることで、現在はネイティブコード呼び出しをトランポリンにすることで実現している。つまり、ネイティブコードから他のSchemeルーチンを呼ぶ場合、
- ネイティブコードの「残りの部分」を実行する継続をVMスタックに積む (1)
- 呼び出したいSchemeルーチンを呼び出すトランポリンコードをVMスタックに積む (2)
- VMへreturn
- (2)経由で目的のSchemeルーチンが呼ばれる (3)
- Schemeルーチンからreturn
- (1)経由でネイティブコードの残りの部分が呼ばれる
ここで、(3)の箇所で継続が捕捉される可能性があるわけだが、再開に必要な情報は全部VMスタックに入っているのでいくらでも再起動できるとうわけ。
しかしこれだと、Scheme関数呼び出しの度にネイティブコードでの関数のエピローグとプロローグが余計に呼ばれることになる。普通のSchemeプログラムでは関数呼び出しは非常に頻繁で、本体が数インストラクションなのにVMスタック操作とプロローグ/エピローグがごそっとついたsubrをたくさん呼び出すことになって、結局速度が上がらない。
アイディアとしては、
- 現行のVMは残すが、subrからの関数呼び出しについては、継続に関する情報をpushするだけで呼び出し自体はCPUのcall/retでやる。継続の呼び出しが無ければネイティブの関数呼び出し/リターンと同じ。継続が呼ばれた場合はlongjmpでCスタックを破棄し、VMにセーブされてた情報から残りの部分を実行する。
- これを行うには、subrで「呼び出しの残り部分」だけを実行するルーチンも別に作っとく必要がある。手でやるのは大変だけど自動生成ならできる。
- VMのスタック操作のオーバーヘッドは避けられない。
- 現在のVMインストラクションベースのコードとの混在が可能。
- 現在のVMを捨てて総とっかえ
- Cheney on the MTAとかね(ただ、VMスタック操作は無くなるけど関数プロローグのオーバヘッドはかかるな)
- 多分こっちの方が綺麗になる
- 現在のVMインストラクション列は直接実行できなくなるので、JITで変換とか
- 現在のVMを想定しているツールは(プロファイラとか)は書き直すことに