common-lisptail-recursionsbclclisp

Common Lisp: Why does my tail-recursive function cause a stack overflow?


I have a problem in understanding the performance of a Common Lisp function (I am still a novice). I have two versions of this function, which simply computes the sum of all integers up to a given n.

Non-tail-recursive version:

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

Tail-recursive version:

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

I am trying to run these functions in CLISP with input n = 1000000. Here is the result

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

*** - Program stack overflow. RESET

I can run both successfully in SBCL, but the non-tail-recursive one is faster (only by a little, but that seems strange to me). I've scoured Stackoverflow questions for answers but couldn't find something similar. Why do I get a stack overflow although the tail-recursive function is designed NOT to put all recursive function calls on the stack? Do I have to tell the interpreter/compiler to optimise tail calls? (I read something like (proclaim '(optimize (debug 1)) to set the debug level and optimize at the cost of tracing abilities, but I don't know what this does). Maybe the answer is obvious and the code is bullshit, but I just can't figure it out. Help is appreciated.

Edit: danlei pointed out the typo, it should be a call to addup3 in the first function, so it is recursive. If corrected, both versions overflow, but not his one

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

While it may be a more typical way to do it, I find it strange that tail recursion is not always optimised, considering my instructors like to tell me it's so much more efficient and stuff.


Solution

  • There is no requirement for a Common Lisp implementation to have tail call optimization. Most do, however (I think that ABCL does not, due to limitations of the Java virtual machine).

    The documentation of the implementation should tell you what optimization settings should be chosen to have TCO (if available).

    It is more idiomatic for Common Lisp code to use one of the looping constructs:

    (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))
    

    In this case, of course, it is better to use the "little Gauß":

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