diff options
| author | Stefan Monnier | 2021-01-08 19:59:16 -0500 |
|---|---|---|
| committer | Stefan Monnier | 2021-01-08 19:59:31 -0500 |
| commit | 29c7f8c915c3889dfd5b25878aa0692f826cd38f (patch) | |
| tree | 50eebd6fdd68eff7398f4d20c040c17752cd9933 | |
| parent | 6e73e07a6f5cbdd1c5ae6e0f3fbd0f8f56813f1a (diff) | |
| download | emacs-29c7f8c915c3889dfd5b25878aa0692f826cd38f.tar.gz emacs-29c7f8c915c3889dfd5b25878aa0692f826cd38f.zip | |
* lisp/emacs-lisp/cl-macs.el: Optimize self-calls in tail position
Implement a limited form of tail-call optimization for the special
case of recursive functions defined with `cl-labels`. Only self-recursion
is optimized, no attempt is made to handle more complex cases such a mutual
recursion.
The main benefit is to reduce the use of the stack, tho in my limited
tests, this can also improve performance (about half of the way to
a hand-written `while` loop).
(cl--self-tco): New function.
(cl-labels): Use it.
* lisp/subr.el (letrec): Optimize single-binding corner case.
* test/lisp/emacs-lisp/cl-macs-tests.el (cl-macs--labels): Add tests
to check that TCO is working.
| -rw-r--r-- | lisp/emacs-lisp/cl-macs.el | 118 | ||||
| -rw-r--r-- | lisp/subr.el | 11 | ||||
| -rw-r--r-- | test/lisp/emacs-lisp/cl-macs-tests.el | 17 |
3 files changed, 135 insertions, 11 deletions
diff --git a/lisp/emacs-lisp/cl-macs.el b/lisp/emacs-lisp/cl-macs.el index 1cb195d1296..ba634d87bc7 100644 --- a/lisp/emacs-lisp/cl-macs.el +++ b/lisp/emacs-lisp/cl-macs.el | |||
| @@ -2060,10 +2060,98 @@ Like `cl-flet' but the definitions can refer to previous ones. | |||
| 2060 | ((null (cdr bindings)) `(cl-flet ,bindings ,@body)) | 2060 | ((null (cdr bindings)) `(cl-flet ,bindings ,@body)) |
| 2061 | (t `(cl-flet (,(pop bindings)) (cl-flet* ,bindings ,@body))))) | 2061 | (t `(cl-flet (,(pop bindings)) (cl-flet* ,bindings ,@body))))) |
| 2062 | 2062 | ||
| 2063 | (defun cl--self-tco (var fargs body) | ||
| 2064 | ;; This tries to "optimize" tail calls for the specific case | ||
| 2065 | ;; of recursive self-calls by replacing them with a `while' loop. | ||
| 2066 | ;; It is quite far from a general tail-call optimization, since it doesn't | ||
| 2067 | ;; even handle mutually recursive functions. | ||
| 2068 | (letrec | ||
| 2069 | ((done nil) ;; Non-nil if some TCO happened. | ||
| 2070 | (retvar (make-symbol "retval")) | ||
| 2071 | (ofargs (mapcar (lambda (s) (if (memq s cl--lambda-list-keywords) s | ||
| 2072 | (make-symbol (symbol-name s)))) | ||
| 2073 | fargs)) | ||
| 2074 | (opt-exps (lambda (exps) ;; `exps' is in tail position! | ||
| 2075 | (append (butlast exps) | ||
| 2076 | (list (funcall opt (car (last exps))))))) | ||
| 2077 | (opt | ||
| 2078 | (lambda (exp) ;; `exp' is in tail position! | ||
| 2079 | (pcase exp | ||
| 2080 | ;; FIXME: Optimize `apply'? | ||
| 2081 | (`(funcall ,(pred (eq var)) . ,aargs) | ||
| 2082 | ;; This is a self-recursive call in tail position. | ||
| 2083 | (let ((sets nil) | ||
| 2084 | (fargs ofargs)) | ||
| 2085 | (while fargs | ||
| 2086 | (pcase (pop fargs) | ||
| 2087 | ('&rest | ||
| 2088 | (push (pop fargs) sets) | ||
| 2089 | (push `(list . ,aargs) sets) | ||
| 2090 | ;; (cl-assert (null fargs)) | ||
| 2091 | ) | ||
| 2092 | ('&optional nil) | ||
| 2093 | (farg | ||
| 2094 | (push farg sets) | ||
| 2095 | (push (pop aargs) sets)))) | ||
| 2096 | (setq done t) | ||
| 2097 | `(progn (setq . ,(nreverse sets)) | ||
| 2098 | :recurse))) | ||
| 2099 | (`(progn . ,exps) `(progn . ,(funcall opt-exps exps))) | ||
| 2100 | (`(if ,cond ,then . ,else) | ||
| 2101 | `(if ,cond ,(funcall opt then) . ,(funcall opt-exps else))) | ||
| 2102 | (`(cond . ,conds) | ||
| 2103 | (let ((cs '())) | ||
| 2104 | (while conds | ||
| 2105 | (pcase (pop conds) | ||
| 2106 | (`(,exp) | ||
| 2107 | (push (if conds | ||
| 2108 | ;; This returns the value of `exp' but it's | ||
| 2109 | ;; only in tail position if it's the | ||
| 2110 | ;; last condition. | ||
| 2111 | `((setq ,retvar ,exp) nil) | ||
| 2112 | `(,(funcall opt exp))) | ||
| 2113 | cs)) | ||
| 2114 | (exps | ||
| 2115 | (push (funcall opt-exps exps) cs)))) | ||
| 2116 | (if (eq t (caar cs)) | ||
| 2117 | `(cond . ,(nreverse cs)) | ||
| 2118 | `(cond ,@(nreverse cs) (t (setq ,retvar nil)))))) | ||
| 2119 | ((and `(,(or 'let 'let*) ,bindings . ,exps) | ||
| 2120 | (guard | ||
| 2121 | ;; Note: it's OK for this `let' to shadow any | ||
| 2122 | ;; of the formal arguments since we will only | ||
| 2123 | ;; setq the fresh new `ofargs' vars instead ;-) | ||
| 2124 | (let ((shadowings (mapcar #'car bindings))) | ||
| 2125 | ;; If `var' is shadowed, then it clearly can't be | ||
| 2126 | ;; tail-called any more. | ||
| 2127 | (not (memq var shadowings))))) | ||
| 2128 | `(,(car exp) ,bindings . ,(funcall opt-exps exps))) | ||
| 2129 | (_ | ||
| 2130 | `(progn (setq ,retvar ,exp) nil)))))) | ||
| 2131 | |||
| 2132 | (let ((optimized-body (funcall opt-exps body))) | ||
| 2133 | (if (not done) | ||
| 2134 | (cons fargs body) | ||
| 2135 | ;; We use two sets of vars: `ofargs' and `fargs' because we need | ||
| 2136 | ;; to be careful that if a closure captures a formal argument | ||
| 2137 | ;; in one iteration, it needs to capture a different binding | ||
| 2138 | ;; then that of other iterations, e.g. | ||
| 2139 | (cons | ||
| 2140 | ofargs | ||
| 2141 | `((let (,retvar) | ||
| 2142 | (while (let ,(delq nil | ||
| 2143 | (cl-mapcar | ||
| 2144 | (lambda (a oa) | ||
| 2145 | (unless (memq a cl--lambda-list-keywords) | ||
| 2146 | (list a oa))) | ||
| 2147 | fargs ofargs)) | ||
| 2148 | . ,optimized-body)) | ||
| 2149 | ,retvar))))))) | ||
| 2150 | |||
| 2063 | ;;;###autoload | 2151 | ;;;###autoload |
| 2064 | (defmacro cl-labels (bindings &rest body) | 2152 | (defmacro cl-labels (bindings &rest body) |
| 2065 | "Make local (recursive) function definitions. | 2153 | "Make local (recursive) function definitions. |
| 2066 | Each definition can take the form (FUNC ARGLIST BODY...) where | 2154 | +BINDINGS is a list of definitions of the form (FUNC ARGLIST BODY...) where |
| 2067 | FUNC is the function name, ARGLIST its arguments, and BODY the | 2155 | FUNC is the function name, ARGLIST its arguments, and BODY the |
| 2068 | forms of the function body. FUNC is defined in any BODY, as well | 2156 | forms of the function body. FUNC is defined in any BODY, as well |
| 2069 | as FORM, so you can write recursive and mutually recursive | 2157 | as FORM, so you can write recursive and mutually recursive |
| @@ -2075,17 +2163,33 @@ details. | |||
| 2075 | (let ((binds ()) (newenv macroexpand-all-environment)) | 2163 | (let ((binds ()) (newenv macroexpand-all-environment)) |
| 2076 | (dolist (binding bindings) | 2164 | (dolist (binding bindings) |
| 2077 | (let ((var (make-symbol (format "--cl-%s--" (car binding))))) | 2165 | (let ((var (make-symbol (format "--cl-%s--" (car binding))))) |
| 2078 | (push (list var `(cl-function (lambda . ,(cdr binding)))) binds) | 2166 | (push (cons var (cdr binding)) binds) |
| 2079 | (push (cons (car binding) | 2167 | (push (cons (car binding) |
| 2080 | (lambda (&rest args) | 2168 | (lambda (&rest args) |
| 2081 | (if (eq (car args) cl--labels-magic) | 2169 | (if (eq (car args) cl--labels-magic) |
| 2082 | (list cl--labels-magic var) | 2170 | (list cl--labels-magic var) |
| 2083 | (cl-list* 'funcall var args)))) | 2171 | (cl-list* 'funcall var args)))) |
| 2084 | newenv))) | 2172 | newenv))) |
| 2085 | (macroexpand-all `(letrec ,(nreverse binds) ,@body) | 2173 | ;; Don't override lexical-let's macro-expander. |
| 2086 | ;; Don't override lexical-let's macro-expander. | 2174 | (unless (assq 'function newenv) |
| 2087 | (if (assq 'function newenv) newenv | 2175 | (push (cons 'function #'cl--labels-convert) newenv)) |
| 2088 | (cons (cons 'function #'cl--labels-convert) newenv))))) | 2176 | ;; Perform self-tail call elimination. |
| 2177 | (setq binds (mapcar | ||
| 2178 | (lambda (bind) | ||
| 2179 | (pcase-let* | ||
| 2180 | ((`(,var ,sargs . ,sbody) bind) | ||
| 2181 | (`(function (lambda ,fargs . ,ebody)) | ||
| 2182 | (macroexpand-all `(cl-function (lambda ,sargs . ,sbody)) | ||
| 2183 | newenv)) | ||
| 2184 | (`(,ofargs . ,obody) | ||
| 2185 | (cl--self-tco var fargs ebody))) | ||
| 2186 | `(,var (function (lambda ,ofargs . ,obody))))) | ||
| 2187 | (nreverse binds))) | ||
| 2188 | `(letrec ,binds | ||
| 2189 | . ,(macroexp-unprogn | ||
| 2190 | (macroexpand-all | ||
| 2191 | (macroexp-progn body) | ||
| 2192 | newenv))))) | ||
| 2089 | 2193 | ||
| 2090 | ;; The following ought to have a better definition for use with newer | 2194 | ;; The following ought to have a better definition for use with newer |
| 2091 | ;; byte compilers. | 2195 | ;; byte compilers. |
diff --git a/lisp/subr.el b/lisp/subr.el index bc0c4179904..260202945b1 100644 --- a/lisp/subr.el +++ b/lisp/subr.el | |||
| @@ -1893,9 +1893,14 @@ all symbols are bound before any of the VALUEFORMs are evalled." | |||
| 1893 | `(let ,(mapcar #'car binders) | 1893 | `(let ,(mapcar #'car binders) |
| 1894 | ,@(mapcar (lambda (binder) `(setq ,@binder)) binders) | 1894 | ,@(mapcar (lambda (binder) `(setq ,@binder)) binders) |
| 1895 | ,@body)))) | 1895 | ,@body)))) |
| 1896 | (if seqbinds | 1896 | (cond |
| 1897 | `(let* ,(nreverse seqbinds) ,nbody) | 1897 | ;; All bindings are recursive. |
| 1898 | nbody)))) | 1898 | ((null seqbinds) nbody) |
| 1899 | ;; Special case for trivial uses. | ||
| 1900 | ((and (symbolp nbody) (null (cdr seqbinds)) (eq nbody (caar seqbinds))) | ||
| 1901 | (nth 1 (car seqbinds))) | ||
| 1902 | ;; General case. | ||
| 1903 | (t `(let* ,(nreverse seqbinds) ,nbody)))))) | ||
| 1899 | 1904 | ||
| 1900 | (defmacro dlet (binders &rest body) | 1905 | (defmacro dlet (binders &rest body) |
| 1901 | "Like `let*' but using dynamic scoping." | 1906 | "Like `let*' but using dynamic scoping." |
diff --git a/test/lisp/emacs-lisp/cl-macs-tests.el b/test/lisp/emacs-lisp/cl-macs-tests.el index 7774ed3145b..bcd63f73a3c 100644 --- a/test/lisp/emacs-lisp/cl-macs-tests.el +++ b/test/lisp/emacs-lisp/cl-macs-tests.el | |||
| @@ -616,6 +616,21 @@ collection clause." | |||
| 616 | ;; Simple recursive function. | 616 | ;; Simple recursive function. |
| 617 | (cl-labels ((len (xs) (if xs (1+ (len (cdr xs))) 0))) | 617 | (cl-labels ((len (xs) (if xs (1+ (len (cdr xs))) 0))) |
| 618 | (should (equal (len (make-list 42 t)) 42))) | 618 | (should (equal (len (make-list 42 t)) 42))) |
| 619 | ) | 619 | |
| 620 | ;; Simple tail-recursive function. | ||
| 621 | (cl-labels ((len (xs n) (if xs (len (cdr xs) (1+ n)) n))) | ||
| 622 | (should (equal (len (make-list 42 t) 0) 42)) | ||
| 623 | ;; Should not bump into stack depth limits. | ||
| 624 | (should (equal (len (make-list 42000 t) 0) 42000))) | ||
| 625 | |||
| 626 | ;; Check that non-recursive functions are handled more efficiently. | ||
| 627 | (should (pcase (macroexpand '(cl-labels ((f (x) (+ x 1))) (f 5))) | ||
| 628 | (`(let* ,_ (funcall ,_ 5)) t))) | ||
| 629 | |||
| 630 | ;; Case of "tail-recursive lambdas". | ||
| 631 | (should (pcase (macroexpand | ||
| 632 | '(cl-labels ((len (xs n) (if xs (len (cdr xs) (1+ n)) n))) | ||
| 633 | #'len)) | ||
| 634 | (`(function (lambda (,_ ,_) . ,_)) t)))) | ||
| 620 | 635 | ||
| 621 | ;;; cl-macs-tests.el ends here | 636 | ;;; cl-macs-tests.el ends here |