Gauche:Applyの消去
(Shiro:2019/03/29 04:35:02 UTC) 0.9.8に入る最適化。
define-inline
は性能を犠牲にせずに抽象化できる便利ツール。
(define-inline (add10 x) (+ 10 x))
と定義しておくと、次のとおり+
を直接使ったのと同じコードになるし:
gosh> (disasm (lambda (y) (add10 y))) CLOSURE #<closure (#f y)> === main_code (name=#f, code=0x2753c30, size=2, const=0 stack=0): signatureInfo: ((#f y)) 0 LREF0-NUMADDI(10) ; (+ x 10) 1 RET
引数が定数なら畳み込まれる:
gosh> (disasm (lambda () (add10 100))) CLOSURE #<closure (#f)> === main_code (name=#f, code=0x2753b00, size=2, const=0 stack=0): signatureInfo: ((#f)) 0 CONSTI(110) 1 RET
(こういう展開では、add10
が再定義されないという情報の方が本質で、それさえ分かっていれば
define-inline
のような特別なフォームを用意する必要はない。
例えばr6rsのようにclosedなモジュールで後からの再定義を禁止するセマンティクスであれば、
モジュール定義を見ただけで再定義されない束縛が分かる。
ただGaucheの場合、モンキーパッチで挙動を変えられるというのも利点のひとつなので、
プログラマが「この束縛は変えない(からインライン展開してよい)」というのを明示する手段として
define-inline
を用意している。)
ところが、可変長引数を扱おうとすると、apply
を経由することになる:
(define-inline (add10n . args) (apply + 10 args))
apply
自体には専用のインストラクションが用意されているのだが、
+
は単なる関数として渡されてしまうのでインライン展開されない:
;; 0.9.7 gosh> (disasm (lambda (y) (add10n y))) CLOSURE #<closure (#f y)> === main_code (name=#f, code=0x26109c0, size=8, const=1 stack=5): signatureInfo: ((#f y)) 0 LREF0 ; y 1 LIST(1) 2 PUSH-LOCAL-ENV(1) ; (add10n y) 3 GREF-PUSH #<identifier user#+.2761900>; + 5 LREF0 ; args 6 TAIL-APPLY(2) ; (apply + args) 7 RET
けれども、コードの字面上ではargs
の長さは決まっているのだから、
もうちょっと気を利かせてくれても良さそうではないか。
0.9.8は気が利くようになった。
;; 0.9.8_pre1 gosh> (disasm (lambda (y) (add10n y))) CLOSURE #<closure (#f . _)> === main_code (name=#f, code=0x7fb49be29b50, size=2, const=0 stack=0): signatureInfo: ((#f . _)) 0 LREF0-NUMADDI(10) ; (apply + 10 args) 1 RET
畳み込みもばっちり。
gosh> (disasm (lambda () (add10n 100))) CLOSURE #<closure (#f . _)> === main_code (name=#f, code=0x7fb49c3e0d20, size=2, const=0 stack=0): signatureInfo: ((#f . _)) 0 CONSTI(110) 1 RET
この展開には他にも連鎖的なメリットがある。ローカル関数は、それ自身が他のグローバル関数に
渡されていると、クロージャにする必要があるんだけれど、apply
に渡されていたのが
通常の関数呼び出しに展開されるとローカル関数自身もさらにインライン展開できて
クロージャが消せるとか。
具体的に追加したのはpass2で、($asm (TAIL-APPLY n) ...)
ノードに出会ったら
その引数を手繰って、コンパイル時点で既に引数の数が確定していたら
$call
ノードに書き換えてる。