Saturday, June 26, 2010

Tail Recursion

There was a conversation at work a few weeks back on the difference between recursion and iteration. Someone made the claim "Recursion doesn't always work," and pointed out that the Fibonacci Sequence, while easy to implement in naive recursion, tends to blow up when the numbers get large.

I argued that there is an efficient solution using tail-recursion. It's faster than naive recursion, and simpler than an iterative solution.

A third person pointed out tail-recursion is a Scheme thing, and not all languages properly optimize it. This is completely true, but... it's also true that tail-recursive algorithms are in principle more efficient. Abelson and Sussman point out that doesn't always translate into actual performance, though:
most implementations of common languages (including Ada, Pascal, and C) are designed in such a way that the interpretation of any recursive procedure consumes an amount of memory that grows with the number of procedure calls, even when the process described is, in principle, iterative. As a consequence, these languages can describe iterative processes only by resorting to special-purpose ``looping constructs'' such as do, repeat, until, for, and while.

So here's a short test... I wrote a short tail-recursive Fib generator in Common Lisp. Note it takes a number (i.e. the number of terms to generate) and returns a list representing the sequence:

(defun fibonacci-sequence (num)
"Calculate a Fibonacci sequence to a number NUM."
(labels ((fib (n acc)
(cond ((equalp n 0) acc)
(T (fib (1- n)
(cons (+ (car acc)
(cadr acc)) acc))))))
(cond ((= num 0) '(1))
((= num 1) '(1 1))
((> 0 num) 'undefined)
(T (reverse (fib (- num 2) '(1 1)))))))

We can loosely translate that to Perl. Perl doesn't have an equivalent to Common Lisp's 'labels', so I had to write two named functions to implement it. But this short script is more-or-less equivalent to the Lisp version: it takes a number on the command line and prints a list representing a sequence with that number of terms:


=head1 NAME

=head1 AUTHOR

Clumsy Ox



Calculates the Fibonacci Sequence to $NUMBER terms.

This is just an exercise in tail-recursion.


use strict;
use warnings;

use Data::Dumper;

my $number = shift;

my @sequence = fibonacci ($number);

print STDOUT join (', ', @sequence), "\n";

=head2 fibonacci

fibonacci ($number) => @sequence

sub fibonacci {
my $num = shift;

return (1) if $num == 0;
return (1, 1) if $num == 1;

return reverse fibt( $num - 2, 1, 1);

=head2 fibt

fibt ($number) => @sequence

sub fibt {
my $num = shift;
return @_ if $num == 0;
return fibt ( $num - 1, $_[0] + $_[1], @_);

Notice both solutions accumulate the sequence as a list. So there is some definite overhead in carrying that sort of data structure, but it's "fair" in the sense that both are having to do it.

Just informally checking it, the Perl solution is slower than the Lisp solution, but not by an amazing amount. I ran a quick-n-dirty test of the Perl solution, and it timed out reasonably. But I found it blew Perl's number stack very quickly and went to 'inf':

bash-3.2$ time ./ 10000 > /tmp/output
Deep recursion on subroutine "main::fibt" at ./ line 56.

real 0m1.072s
user 0m0.640s
sys 0m0.380s

Just for comparison, Lisp returned:

(time (fib-sequence 10000))
Evaluation took:
0.019 seconds of real time
0.018860 seconds of total run time (0.012529 user, 0.006331 system)
[ Run times consist of 0.010 seconds GC time, and 0.009 seconds non-GC time. ]
100.00% CPU
47,236,537 processor cycles
4,893,824 bytes consed

Both took longer to print the result than to actually calculate it.

I haven't tried the experiment in Java or C, but I think it might be interesting to see what would happen.

Incidentally, according to SBCL, the 10,000th element of the Fibonacci Sequence is a 2090 digit number:

(format t "~:d" (car (last (fib-sequence 10000))))

Update: I decided to try a quick-n-dirty Java implementation. It's rough and ugly, but it seems to work.

Here's where it gets interesting: the Java solution works just fine, but it's slow, and it runs into a StackOverflow at just over 10,000 elements. I suspect that could be increased dramatically with some better java runtime settings, but it seems to disprove my thesis that tail recursion is an appropriate solution regardless of implementation language.

I was also able to get Perl to give me better results with
use bignum;
Still, Lisp is by far the fasest solution.


freedomnan said...

When I read, at the beginning of this post: "a conversation at work a few weeks back on the difference between recursion and iteration", I thought - grammar - and wondered what "recursion" was!

Dave Hingsburger said...

um, what?