Gauche:Applyの消去

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

More ...