再帰的に自己を一回だけインライン展開する — #:g1

Posted 2017-10-29 23:34:38 GMT

日立が開発していたメインフレーム用Common LispであるHiLISP(VOS3 LISP)についての記事・知識処理用言語HiLISPの高速化方式 — 日立評論 1987年3月号を読んでいて、再帰呼び出しを一回だけインライン展開するという方法が紹介されているのを目にした(記事では、自己再帰展開と呼んでいる)。

面白そうなのでこの自己再帰展開™というものを再現してみることにした。

最近のCommon Lispの処理系ではインライン指定すれば再帰的に展開してくれるものもあれば、そうでないものもあり、展開の仕方もまちまちだけれどもfletで関数内関数を定義し、それにインライン指定すれば簡単に実現できると思われる。

(defmacro defsubstself (name (&rest args) &body body)
  `(defun ,name (,@args)
     (flet ((,name (,@args)
              ,@body))
       (declare (inline ,name))
       ,@body)))

(defsubstself fib/ (n)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (fib/ (1- n))
              (fib/ (- n 2))))))
===>
(defun fib/ (n)
  (flet ((fib/ (n)
           (declare (optimize (speed 3) (safety 0) (debug 0)))
           (declare (fixnum n))
           (the fixnum (if (< n 2) n (+ (fib/ (1- n)) (fib/ (- n 2)))))))
    (declare (inline fib/))
    (declare (optimize (speed 3) (safety 0) (debug 0)))
    (declare (fixnum n))
    (the fixnum (if (< n 2) n (+ (fib/ (1- n)) (fib/ (- n 2)))))))

fletの関数定義部のボディの中で自己を参照することはないので、一回だけ展開させるには都合が良い。

自己再帰展開™fibと通常のfibを比べるとLispWorksでは25%程高速化した。
めでたしめでたし。

解決かと思ったが……

fletを使えばできると思ったが、しかし、大域関数をfuncallした場合は、どうなるだろう。
もちろんfletで作った関数は大域関数ではないので呼ぶことはできない。
具体的には下記のような場合。

(defun fib (n)
  (declare (optimize (speed 3) (debug 3) (safety 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (funcall 'fib (1- n))
              (funcall 'fib (- n 2))))))

そもそも、こういう場合は、インライン展開はどうなるのだろうか。
インライン展開してくれそうな処理系で調べてみると、fibinline宣言がされた場合の挙動は、

処理系 funcall 'fn funcall #'fn
LispWorks 展開する 展開する
SBCL 展開しない 展開する
Allegro CL 展開しない 展開しない

という風に、展開したりしなかったりの様子。
funcall 'fnでもコンパイル時にはインライン指定のスコープはfuncall #'fnと同様に確定できると思われるので、SBCLが展開しないのはちょっと不思議。

3.2.2.3 Semantic Constraints にも

Within a function named F, the compiler may (but is not required to)
assume that an apparent recursive call to a function named F refers to
the same definition of F, unless that function has been declared
notinline. The consequences of redefining such a recursively defined
function F while it is executing are undefined.

とあるので、コンパイル時に確定はできそうだけれど、an apparent recursive call to a function named Ffuncall 'fn形式が含まれるのかは良く分からない。
(functionの定義からして、funcall #'functionは含まれるだろう)
もしかすると、LispWorksがやりすぎなのかもしれない。

ちなみに、Allegro CLはインラインについて一家言あるようなので展開しないらしい。

LispWorksでは、funcall 'fnでもインライン展開するので、大域の補助関数を作成して、それを呼び出せば良さそうだけれど、SBCLではそれでは駄目なので、結局はコードウォーキングしないといけないらしい。

ということで処理系に備わっているwalk-formを使って下記のようなものを書いてみた。

(import (find-symbol (string '#:walk-form)
                     #+allegro :excl
                     #+sbcl :sb-walker
                     #+lispworks :walker))

(defun replace-fn (fsym replace form env) (let ((mark (gensym "mark"))) (subst replace mark (walk-form form env (lambda (sub cxt env) (declare (ignore cxt env)) (when (and (consp sub)) (when (eq fsym (car sub)) (setf (car sub) mark)) (when (and (eq 'function (car sub)) (eq fsym (cadr sub))) (setf (cadr sub) mark)) (when (or (eq 'funcall (car sub)) (eq 'apply (car sub))) (when (and (eq 'quote (caadr sub)) (eq fsym (cadadr sub))) (setf (caadr sub) 'function) (setf (cadadr sub) mark)))) sub)))))

(defmacro defsubstself (name (&rest args) &body body &environment env) (replace-fn name `(lambda (,@args) ,@(copy-tree body)) `(defun ,name (,@args) ,@body) env))

これは、自分の関数名の所をlambdaで置き換えてしまうので下記のような展開になる。
(なお、一度シンボルで置き換えてからlambdaフォームに直しているのは、walk-formが置き換えたフォームを更に展開し展開が止まらなくなるため。)
上記では、lambdaで展開してしまったが、fletfibを定義し、funcall 'fibfuncall #'fibに書き換えても良いと思う。

(defsubstself fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (funcall 'fib (1- n))
              (funcall 'fib (- n 2))))))
===>
(defun fib (n)
  ...
 (the fixnum
      (if (< n 2)
          n
          (+ (funcall #'(lambda (n)
                          (declare (optimize
                                    (speed 3)
                                    (safety 0)
                                    (debug 0)))
                          (declare (fixnum n))
                          (the fixnum
                               (if (< n 2)
                                   n
                                   (+ (funcall 'fib
                                               (1- n))
                                      (funcall 'fib
                                               (- n
                                                  2))))))
                      (1- n))
             (funcall #'(lambda (n)
                          (declare (optimize
                                    (speed 3)
                                    (safety 0)
                                    (debug 0)))
                          (declare (fixnum n))
                          (the fixnum
                               (if (< n 2)
                                   n
                                   (+ (funcall 'fib
                                               (1- n))
                                      (funcall 'fib
                                               (- n
                                                  2))))))
                      (- n 2)))))))

こうするとSBCLでも30%程の高速化となった。
ちなみに、Allegro CLだと普通に書いたものより遅くなるため、余計なことはしない方が良いらしい。

結び

HiLISPがfuncall 'fnをどう解釈するのかは分からないので、展開する場合としない場合を考えてみた。

Common Lispではインライン指定は処理系によって任意に解釈できることは知っていたが、調べてみると結構ばらばらなんだなと思った次第。
ちなみにSBCLでは、再帰的な局所関数ではlabels+inline指定をすると展開がかなり効く模様。
なお、大抵の処理系では、今回のような手作りの展開で速くなるが、Allegro CLの場合は、別の作法があるらしく、寧ろずっと遅くなるので注意。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus