aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefan Monnier2021-01-08 19:59:16 -0500
committerStefan Monnier2021-01-08 19:59:31 -0500
commit29c7f8c915c3889dfd5b25878aa0692f826cd38f (patch)
tree50eebd6fdd68eff7398f4d20c040c17752cd9933
parent6e73e07a6f5cbdd1c5ae6e0f3fbd0f8f56813f1a (diff)
downloademacs-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.el118
-rw-r--r--lisp/subr.el11
-rw-r--r--test/lisp/emacs-lisp/cl-macs-tests.el17
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.
2066Each definition can take the form (FUNC ARGLIST BODY...) where 2154+BINDINGS is a list of definitions of the form (FUNC ARGLIST BODY...) where
2067FUNC is the function name, ARGLIST its arguments, and BODY the 2155FUNC is the function name, ARGLIST its arguments, and BODY the
2068forms of the function body. FUNC is defined in any BODY, as well 2156forms of the function body. FUNC is defined in any BODY, as well
2069as FORM, so you can write recursive and mutually recursive 2157as 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