CSC447

Concepts of Programming Languages

Tail Recursion

Instructor: James Riely

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 - 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 head; node *tail; };

int sum_aux (node *x, int result) {
  if (!x) {
    return result;
  } else {
    return sum_aux (x->tail, result + x->head);
  }
}

int sum (node *x) {
  return sum_aux (x, 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
          

Exponential Growth - Scala

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

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