IForm を視る - Scheme VM を書く
VM の設計上、A正規形だと let が増えすぎて性能向上が見込めないのではないかと仮結論を出した。
中間表現として Gauche の IForm (もしくは似たもの)を採用するのはどうだろうか。
そもそもの中間表現選びの動機は
- インライン展開などの最適化をやりやすくすること
- とにかくコンパイル時間を短く
- 実行速度を速く
である。
一番の心配というか不明瞭な点はインライン展開の方法なので Gauche のコードを追ってみる。
compile.scm の pass1 のコメントには
;;; - Macros and global inlinable functions are expanded.
この inlinable functions が何者で、どのように expand されるかを調べる必要がありそう。
pass1 手続きの中でS式が (op . args) のように手続きっぽい形式をしている場合、pass1/lookup-head 後続の処理の振り分けが行われます。
pass1/lookup-head は compile 時の env を利用し、op が結びついている identifier、ローカル変数、ローカルマクロのいずれかを返します。
実際には lookup では intlib.stub で定義されている cenv-lookup が利用されます。
さて op が identifier であるとき、(pass1/global-call op) が呼ばれます。
op を引数に global-call-type が呼ばれ、inline 可能な手続きである場合、pass1/expand-inliner が呼ばれます。
pass1/expand-inliner で展開されるのは
;; Expand inlinable procedure. Inliner may be... ;; - An integer. This must be the VM instruction number. ;; (It is useful to initialize the inliner statically in .stub file). ;; - A vector. This must be a packed intermediate form. It is set if ;; the procedure is defined by define-inline. ;; - A procedure. It is called like a macro expander. ;; It may return #<undef> to cancel inlining.
のようなもの。
ということは、つまりユーザーが define した手続きは pass1時のインライン対象外かな。
実験してみよう。
(define (func x) 3) => ($define () func ($lambda[func;0] (x[0;0]) ($const 3))) (func 4) => ($call ($gref func) ($const 4))
どうやらそのようだ。
pass2を見てみるか。あれ pass2 でも inline 展開されないな。
internal define はどうだろう?
(define (func x) (define (i-func y) y) (i-func x)) ==>pass1 ($define () func ($lambda[func;0] (x[1;0]) ($letrec ((i-func[1;0] ($lambda[i-func;0] (y[1;0]) ($lref y[1;0]))) ) ($call ($lref i-func[1;0]) ($lref x[1;0]))))) ==>pass2 ($define () func ($lambda[func;0] (x[2;0]) ($lref x[2;0])))
お。最適化された。
もしかして僕は大きな勘違いをしているのだろうか。
ああ。グローバルに define されたものは後から set! される可能性があるから inline 展開しないのか。今更気づくなよ。。
そしてそのための define-inlineなのか!
続く。