Gauche pass3 メモ - $CALL
関連記事。
コメントの訳
$CALL
$CALL ノードにはいくつかのバリエーションがある。それらには更に tail-call 版と non-tail-call 版があるだろう。
1. Local call:
'local' フラグをもつ $CALL ノードでローカル手続きの call を表す。呼び出し時の引数は手続きのうけとる引数の形に調整済みである。
PROC スロットにはローカル手続きを指す $LREF ノードが格納されている。
2. Embedded call:
'embed' フラグを持つ $CALL ノードで2つ以上の場所から呼び出されるエントリポイントを持つ。(1回しか呼び出されない手続きは $LET ノードになっているので、ここでは考えなくてよい)。
呼び出し時の引数は手続きのうけとる引数の形に調整済みである。
PROC スロットには embedded(埋め込まれた)$LAMBDA ノードが格納される。embedded $LAMBDA ノードの body は $LABEL ノードである。
生成されるコードは $LET ノードのそれとほとんど同じである。違うのは label が LOCAL-ENV の直後に置かれることである。
またこのノードに RENV を保持する。RENV は後で jump call ノードにおいていくつの environment frame を破棄すべきかを決定するのに使われる。
(ここでは embed ノードは関連する jump ノードの前に pass3 に渡されることを前提としている)。
3. Jump call:
'jump' フラグを持つ $CALL ノードでインライン展開されたローカル手続きへに移動することを表すノードである。この $CALL ノードの body はどこか別の場所に 'embedded call' ノードにより埋め込まれている。
PROC スロットは embed $CALL ノードを格納している。
このノードをコンパイルすると LOCAL-ENV-JUMP インストラクションになる。
4. Head-heavy call:
フラグがない $CALL ノードで全ての引数がシンプルな式(const か lref)であり、operator が $LET であるもの。
通常の call の流れでは引数の push のあとに operator が評価される。これにより $LET は 'top' context で評価されることになり、余計な continuation を push することが必要になる。
もし全ての引数がシンプルであるならば operator を先に評価し、引数の評価時にも VAL0 にその値を保持し続けることができる。
特に目立って名前付きlet が head-heavy call になる傾向があり、これを特別扱いする価値がある。
この head-heavy call 最適化は CONST-PUSH、LREF-PUSH などの合成インストラクション が使われることが前提である。
なぜならばもしこれらの合成が off になっているばあい VAL0 が引数で上書きされてしまうからである。
5. その他の call ノードは標準的な call のコードとしてコンパイルされる。
embedded と jump の違い
最初考えたときは embedded と jump の違いが分からなかった。すべて embedded で良いではないかと思ったが勘違い。
embedded の目的はクロージャを作らずインライン展開することだけでなく、埋め込まれたものを複数の箇所で共有すること。
例えば3箇所から同じクロージャが call されていれば1つは embed にして残り2つはその embed に対する jump にすれば良いというからくり(のはず)。
lambda の最適化
$lambda.calls に call site が記録してあるので、それぞれを locals recs tail-recs に分類する。
(1)local が1つ、それ以外は tail-rec の場合、local は embeded に、tail-rec は jump になる。
(2)recもtail-rec もなくて local のみの場合、サイズが小さいクロージャならすべてインライン展開される。