Concepts of Programming Languages

Tail Recursion

Instructor: Stefan Mitsch

Call Stack

  • Contains activation records (AR) for active calls
    • also known as stack frames
  • Changes to call stack
    • AR pushed when a function/method call is made
    • AR popped when a function/method returns
  • Runtime environments limit size of call stacks?
    • Can cause problems with deep recursion

Stack Limits - C


int count_down (int x) {
  if (x == 0) {
    return 0;
  } else {
    return 1 + count_down (x - 1);
  }
}

int main (int argc, char **argv) {
  long num = strtol (argv[1], NULL, 10);
  count_down (num);
  return 0;
}
          

$ ./recursion 272001
$ ./recursion 272002
Segmentation fault
$ ./recursion 272001
$ ./recursion 272001
Segmentation fault
$ for x in `seq 261700 261960`; do echo $x; ./recursion $x; done
          

Stack Limits - OS

  • OS specific
  • E.g., Linux kernel limits set via shell's ulimit

$ ulimit -a
...
stack size              (kbytes, -s) 8192
...

$ ulimit -S -s
8192

$ ulimit -H -s
unlimited

$ ulimit -s 65536
$ ./recursion 2000000
$ ./recursion 3000000
Segmentation fault
          

Stack Limits - Scheme


(define (count-down x) 
  (if (= x 0) 
      0
      (+ 1 (count-down (- x 1)))))
          

$ csi

CHICKEN
(c)2008-2011 The Chicken Team
(c)2000-2007 Felix L. Winkelmann
Version 4.7.0 
linux-unix-gnu-x86-64 [ 64bit manyargs dload ptables ]
compiled 2011-10-17 on allspice (Linux)

#;1> (define (count-down x) 
  (if (= x 0) 
      0
      (+ 1 (count-down (- x 1)))))
#;2> (count-down 73000)
73000
#;3> (count-down 75000)
Segmentation fault
          

Stack Limits - Java


static int countDown (int x) {
  if (x == 0) {
    return 0;
  } else {
    return 1 + countDown (x - 1);
  }
}
          

$ java Recursion 18053

$ java Recursion 18054
Exception in thread "main" java.lang.StackOverflowError
  at Recursion.countDown(Recursion.java:6)
  at Recursion.countDown(Recursion.java:6)
  at Recursion.countDown(Recursion.java:6)
  at Recursion.countDown(Recursion.java:6)
  ...
          

Stack Limits - Scala


def countDown (x:Int) : Int = if x == 0 then 0 else 1 + countDown (x - 1)
          

scala> countDown (59085)
res40: Int = 59085

scala> countDown (59086)
java.lang.StackOverflowError
  at .countDown(<console>:7)
  at .countDown(<console>:7)
  at .countDown(<console>:7)
  at .countDown(<console>:7)
  ...
          

Intuitively


def countDown (x:Int) : Int = if x == 0 then 0 else 1 + countDown (x - 1)
          
Each (1 + ...) represents a new AR

countDown (5)
--> 1 + countDown (4)
--> 1 + (1 + countDown (3))
--> 1 + (1 + (1 + countDown (2)))
--> 1 + (1 + (1 + (1 + countDown (1))))
--> 1 + (1 + (1 + (1 + (1 + countDown (0))))) 
--> 1 + (1 + (1 + (1 + (1 + 0))))
            

Why A Call Stack?

More precisely...

int count_down (int x) {
  if (x == 0) {
    return 0;
  } else {
    return 1 + count_down (x - 1);
  }
}
          
...look at assembly language

$ gcc -std=c99 -S recursion.c
            

Assembly Language View


count_down:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        movl    %edi, -4(%rbp)
        cmpl    $0, -4(%rbp)
        jne     .L2
        movl    $0, %eax
        jmp     .L3
.L2:
        movl    -4(%rbp), %eax
        subl    $1, %eax
        movl    %eax, %edi
        call    count_down
        addl    $1, %eax        ; work *after* recursive call
.L3:
        leave
        ret
          

Assembly Language View


int count_down (int x) {
  ...
  return 1 + count_down (x - 1); // work *after* recursive call
  ...
}
          

count_down:
        pushq   %rbp
        movq    %rsp, %rbp
        ...
        call    count_down
        addl    $1, %eax        ; work *after* recursive call
        ...
        leave
        ret
          

Tail Recursive Call - C


// *tail-recursive functions* because all
// recursive calls are tail-recursive
int count_down_aux (int x, int result) {
  if (x == 0) {
    return result;
  } else {
    return count_down_aux (x-1, 1+result); // *tail-recursive call*
  }
}

int count_down (int x) {
  return count_down_aux (x, 0);
}
          

Assembly Language View


count_down_aux:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $16, %rsp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        cmpl    $0, -4(%rbp)
        jne     .L2
        movl    -8(%rbp), %eax
        jmp     .L3
.L2:
        movl    -8(%rbp), %eax
        leal    1(%rax), %edx
        movl    -4(%rbp), %eax
        subl    $1, %eax
        movl    %edx, %esi
        movl    %eax, %edi
        call    count_down_aux     ; nothing afterwards
.L3:
        leave
        ret
          

Optimize Loop Away!


$ gcc -std=c99 -O2 -S recursion.c
          

count_down:
        movl    %edi, %eax
        ret
          

$ gcc -std=c99 -O2 -S tail-recursion.c
          

count_down_aux:
        movl    %esi, %eax
        testl   %edi, %edi
        leal    (%rdi,%rax), %edx
        cmovne  %edx, %eax
        ret
count_down:
        movl    %edi, %eax
        ret
          
Need more complex examples!

Tail Call Optimization

  • Many compilers implement tail-call optimization
  • Recursive calls must be tail-recursive
  • Includes mutual recursion
    • f calls to g, which calls back to f
  • Previous C program too simple to show this
    • sum linked lists instead

Tail Recursive Call - C

  • Refactor work and accumulate result

typedef struct node node;
struct node { int item; node *next; };

int sum_aux (node *data, int result) {
  if (!data) {
    return result;
  } else {
    return sum_aux (data->next, result + data->item);
  }
}

int sum (node *data) {
  return sum_aux (data, 0);
}
          

Optimize to Loop


$ gcc -std=c99 -O2 -S tail-recursion2.c
          

sum_aux:
        testq   %rdi, %rdi
        movl    %esi, %eax
        je      .L7
.L9:
        addl    (%rdi), %eax
        movq    8(%rdi), %rdi
        testq   %rdi, %rdi
        jne     .L9
.L7:
        rep
        ret
          

Tail Recursion - Scheme

  • Implementations perform tail-call optimization
  • For tail-recursive functions
  • More generally, for tail-calls

Exponential Growth - Scheme

  • Need very long linked lists; avoid stack overflow!
  • Is it tail recursive? sublist necessary?

(define (long-list n)
  (if (= n 0) 
      '(1)
      (let ([sublist (long-list (- n 1))])
        (append sublist sublist))))
          

$ csi

#;1> (long-list 0)
(1)
#;2> (long-list 1)
(1 1)
#;3> (long-list 2)
(1 1 1 1)
#;4> (long-list 4)
(1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1)
#;6> (length (long-list 20))
1048576
          

Tail Recursion - Scheme

  • Tail recursive, then recursive summation

(define (sum-tail-recursive xs) 
  (let loop ([xs xs] [result 0])
    (if (eq? xs '()) 
        result
        (loop (cdr xs) (+ (car xs) result)))))

(define (sum xs) 
  (if (eq? xs '()) 
      0
      (+ (car xs) (sum (cdr xs)))))
          

$ csi

#;8> (sum-tail-recursive (long-list 20))
1048576

#;10> (sum (long-list 20))    
Segmentation fault
          

Exponential Growth - Scala

  • As in Scheme

def longList (n:Int) : List[Int] =
  if n == 0 then 
    List (1)
  else
    val sublist = longList (n - 1)
    sublist ::: sublist
          

Tail Recursion - Scala

tailrec annotation

import scala.annotation.tailrec

def sumTailRecursive (xs:List[Int]) : Int = 
  @tailrec 
  def aux (xs:List[Int], result:Int) : Int =
    xs match
      case Nil   => result
      case y::ys => aux (ys, y + result)
  aux (xs, 0)
          

scala> longList (20).length
res0: Int = 1048576

scala> sumTailRecursive (longList (20))
res1: Int = 1048576
          

Tail Recursion - Scala

tailrec annotation fails if not optimized

import scala.annotation.tailrec

def sumTailRecursive (xs:List[Int]) : Int =
  @tailrec 
  def aux (xs:List[Int], result:Int) : Int =
    xs match
      case Nil   => result
      case y::ys => 1 + aux (ys, y + result)   // bogus "1 + ..."
  aux (xs, 0)
          
Scala compiler rejects the code

error: could not optimize @tailrec annotated method aux: 
  it contains a recursive call not in tail position