Fibonacci

Fibonacci with tail recursion in Kotlin

Fibonacci spiral

I enjoyed reading the The Fibonacci Problem blog post that shows (among other things) a tail recursive algorithm for the Fibonacci sequence.

The original code is in Golang which has no tail recursion optimization. I wanted to compare the Golang code to Kotlin, see Kotlin’s tailrec in action and measure the entire thing with jmh.

I retained the original function names (up to capitalization) so the original Golang code:

func FibNaive(n int) int {
    if n < 2 {
        return n
    }   
    return FibNaive(n-1) + FibNaive(n-2)
}

looks like so in my version in Kotlin:

fun fibNaive(n: Int): Int =
        when (n) {
            0, 1 -> n
            else -> fibNaive(n - 1) + fibNaive(n - 2)
        }

Sources are here. To build and run, execute:

mvn clean install && java -jar target/benchmarks.jar

FibCached

Here, we improve on the naive approach by caching the intermediate results:

1fun fibCached(
2    n: Int, 
3    cache: MutableMap<Int, Int> = mutableMapOf(Pair(0, 0), Pair(1, 1))): Int =
4    
5        cache.getOrPut(n) {
6            cache.getOrPut(n - 1) { fibCached(n - 1, cache) } +
7            cache.getOrPut(n - 2) { fibCached(n - 2, cache) }
8        }

This gives us over five orders of magnitude improvement over fibNaive.

The initial implementation worked around Kotlin not understanding my intent, details are on StackOverflow

FibVectorSum

The key observation here is that we can replace two recursive calls each returning a scalar ( i.e. f(n-1) + f(n-2) ) with a single recursive call that returns a vector (a Pair really).

 1fun T(p: Pair<Int, Int>): Pair<Int, Int> = Pair(p.first + p.second, p.first)
 2
 3fun fibVecSum(n: Int): Int {
 4
 5    fun fibVec(n: Int): Pair<Int, Int> =
 6            when (n) {
 7                1 -> Pair(1, 0)
 8                else -> T(fibVec(n - 1))
 9            }
10
11
12    return when (n) {
13        0, 1 -> n
14
15        else -> {
16            val (a, b) = fibVec(n - 1)
17            a + b
18        }
19    }
20}

This gives us an order of magnitude improvement over fibCached.

Incidentally, there is a a more Kotlin-idiomatic way to implement the same approach using the generateSequence function. It doesn’t match the narrative of the original post though:

fun fibVecSumKotlin(n: Int): Int {

    fun genPairSequence(): Sequence<Pair<Int, Int>> =
            generateSequence(Pair(1, 0), { T(it) })

    return genPairSequence().take(n + 1).last().second
}

FibTailVecSum

The remaining issue in FibVectorSum preventing it being tail recursive is the transformation at line 8. This can be fixed by accumulating the sum into the recursive call:

 1fun fibTailVecSum(n: Int): Int {
 2
 3    tailrec fun fibTailVec(acc: Int, a: Int, b: Int): Pair<Int, Int> =            
 4            when (acc) {
 5                1 -> Pair(a, b)
 6                else -> fibTailVec(acc - 1, a + b, a)
 7            }
 8    
 9    return when (n) {
10        0, 1 -> n
11        else -> {
12            val (a, b) = fibTailVec(n - 1, 1, 0)
13            a + b
14        }
15    }
16}

Note the tailrec decoration of the recursive function at line 3. Once again we have an order of magnitude improvement over the previous take; fibVectorSum.

FibIterative

In the iterative version the fibTailVec is replaced by a while loop

fun fibIterative(n: Int): Int {
    if (n < 2) {
        return n
    }

    var acc = n
    var a = 1
    var b = 0
    var tmp = 0

    while (acc > 2) {
        acc--
        tmp = a
        a += b
        b = tmp
    }
    return a + b
}

The code is more verbose than the Go version because Kotlin has no “multiple assignments” as in n, a, b = n-1, a+b, a.

It is also the fastest.

Benchmarks

Benchmarks were generated using jmh with the following configuration:

@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 15)
@Measurement(iterations = 15)
@BenchmarkMode(AverageTime)
@OutputTimeUnit(NANOSECONDS)
Benchmark (n) Score Units Normalaized by fibIterative
fibNaive 12 505.787 ns/op 109
fibNaive 40 359727438.400 ns/op 32702494
fibCached 12 490.736 ns/op 106
fibCached 40 1393.079 ns/op 123
fibVecSum 12 56.979 ns/op 12
fibVecSum 40 242.230 ns/op 21
fibVecSumKotlin 12 99.435 ns/op 21
fibVecSumKotlin 40 273.117 ns/op 24
fibTailVecSum 12 8.447 ns/op 2
fibTailVecSum 40 14.800 ns/op 1
fibIterative 12 4.666 ns/op 1
fibIterative 40 11.372 ns/op 1
fibIterativeTabulated 12 81.520 ns/op 17
fibIterativeTabulated 40 257.801 ns/op 22
kotlin  algo  jmh