Disclaimer: some possibly dubious Haskell ahead.
I've implemented 2 versions of Fibonacci, one that is linear and tail-recursive (incl. a banged variant), and another using the classic lazy-list approach:
{-# LANGUAGE BangPatterns #-}
-- Linear
fibLinear :: Int -> Integer
fibLinear 0 = 0
fibLinear 1 = 1
fibLinear x = fibLin' 2 1 2
where
fibLin' i y z | i == x = y
fibLin' i y z = fibLin' (i+1) z (z+y)
-- Linear banged
fibLinearBang :: Int -> Integer
fibLinearBang 0 = 0
fibLinearBang 1 = 1
fibLinearBang x = fibLin' 2 1 2
where
fibLin' (!i) (!y) (!z) | i == x = y
fibLin' (!i) (!y) (!z) = fibLin' (i+1) z (z+y)
-- Haskeller classic lazy
fibClassic :: Int -> Integer
fibClassic n = fib' !! n
where fib' = 0:1:zipWith (+) fib' (tail fib')
When using criterion to benchmark it (code), I get somewhat unintuitive results: Fib 30 takes 443.0 ns for fibLinear
and 279.5 ns for fibLinearBang
, vs 73.51 ns for fibClassic
. This is strange because I would think fibLinear
& fibLinearBang
could be optimised into something like a loop or a jump.
My only guess is maybe fib'
in fibClassic
is memoised as a constant list despite being inside a function definition so that subsequent invocations of the function reuse the same list for lookup.
Can someone give me a definitive explanation for my benchmark results?
For those interested, I've put the project on Github.