Common Lisp:为什么我的尾递归函数导致堆栈溢出?

时间:2013-05-18 16:50:34

标签: common-lisp tail-recursion sbcl clisp

我在理解Common Lisp函数的性能方面遇到了问题(我仍然是新手)。我有这个函数的两个版本,它只计算所有整数的总和,直到给定的n

非尾递归版:

(defun addup3 (n) 
  (if (= n 0)
    0   
    (+ n (addup (- n 1)))))

尾递归版:

(defun addup2 (n) 
  (labels ((f (acc k)
              (if (= k 0)
                   acc 
                   (f (+ acc k) (- k 1)))))
  (f 0 n)))

我正在尝试使用输入n = 1000000在CLISP中运行这些函数。结果如下

[2]> (addup3 1000000)
500000500000
[3]> (addup2 1000000)

*** - Program stack overflow. RESET

我可以在SBCL中成功运行,但非尾递归的更快(只有一点点,但这对我来说似乎很奇怪)。我已经搜索了Stackoverflow问题的答案,但找不到类似的东西。为什么我的堆栈溢出虽然尾递归函数被设计为不将所有递归函数调用放在堆栈上?我是否必须告诉解释器/编译器优化尾调用? (我读了(proclaim '(optimize (debug 1))之类的内容来设置调试级别并以追踪能力为代价进行优化,但我不知道这是做什么的。 也许答案是显而易见的,代码是胡说八道,但我无法弄清楚。 感谢帮助。

编辑:danlei指出了拼写错误,它应该是在第一个函数中调用addup3,因此它是递归的。如果纠正,两个版本都会溢出,但不是他的一个

(defun addup (n) 
         "Adds up the first N integers"
         (do ((i 0 (+ i 1)) 
              (sum 0 (+ sum i)))
             ((> i n) sum)))

虽然这可能是一种更典型的方式,但我觉得奇怪的是尾递归并不总是优化的,考虑到我的教师喜欢告诉我它的效率和效率都高得多。

3 个答案:

答案 0 :(得分:8)

没有要求Common Lisp实现具有尾调用优化。然而,大多数人都这样做(我认为ABCL没有,因为Java虚拟机的限制)。

实施文档应告诉您应选择哪些优化设置以获得TCO(如果可用)。

Common Lisp代码使用其中一个循环结构更为惯用:

(loop :for i :upto n
      :sum i)

(let ((sum 0))
  (dotimes (i n)
    (incf sum (1+ i))))

(do ((i 0 (1+ i))
     (sum 0 (+ sum i)))
    ((> i n) sum))

在这种情况下,当然,最好使用“小高斯”:

(/ (* n (1+ n)) 2)

答案 1 :(得分:5)

好吧,你的addup3根本不是递归的

(defun addup3 (n) 
  (if (= n 0)
    0   
    (+ n (addup (- n 1))))) ; <--

它会调用addup。在SBCL中尝试更正版本:

CL-USER> (defun addup3 (n) 
           (if (= n 0)
               0   
               (+ n (addup3 (- n 1)))))
ADDUP3
CL-USER> (addup3 100000)
Control stack guard page temporarily disabled: proceed with caution
;  ..
; Evaluation aborted on #<SB-SYS:MEMORY-FAULT-ERROR {C2F19B1}>.

正如您所期望的那样。

答案 2 :(得分:1)

使用GNU CommonLisp,GCL 2.6.12,编译Resource File将优化尾部调用,这是我得到的:

addup2

希望它有所帮助。