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ノードに書き換えてる。