Reasoning about performance in Haskell
functional-programming, haskell, lazy-evaluation, performance
Solution
In your particular Fibonacci example, it's not very hard to see why the second one should run faster (although you haven't specified what f2 is).
It's mainly an algorithmic issue:
- fib1 implements the purely recursive algorithm and (as far as I know) Haskell has no mechanism for "implicit memoization".
- fib2 uses explicit memoization (using the fibArr list to store previously computed values.
In general, it's much harder to make performance assumptions for a lazy language like Haskell, than for an eager one. Nevertheless, if you understand the underlying mechanisms (especially for laziness) and gather some experience, you will be able to make some "predictions" about performance.
Referential transparency increases (potentially) performance in (at least) two ways:
- First, you (as a programmer) can be sure that two calls to the same function will always return the same, so you can exploit this in various cases to benefit in performance.
- Second (and more important), the Haskell compiler can be sure for the above fact and this may enable many optimizations that can't be enabled in impure languages (if you've ever written a compiler or have any experience in compiler optimizations you are probably aware of the importance of this).
If you want to read more about the reasoning behind the design choices (laziness, pureness) of Haskell, I'd suggest reading this.
Problem
The following two Haskell programs for computing the n'th term of the Fibonacci sequence have greatly different performance characteristics: ``` fib1 n = case n of 0 -> 1 1 -> 1 x -> (fib1 (x-1)) + (fib1 (x-2)) fib2 n = fibArr !! n where fibArr = 1:1:[a + b | (a, b) <- zip fibArr (tail fibArr)] ``` They are very close to mathematically identical, but `fib2` uses the list notation to memoize its intermediate results, while `fib1` has explicit recursion. Despite the potential for the intermediate results to be cached in `fib1`, the execution time gets to be a problem even for `fib1 25`, suggesting that the recursive steps are always evaluated. Does referential transparency contribute anything to Haskell's performance? How can I know ahead of time if it will or won't? This is just an example of the sort of thing I'm worried about. I'd like to hear any thoughts about overcoming the difficulty inherent in reasoning about the performance of a lazily-executed, functional programming language. Summary: I'm accepting 3lectrologos's answer, because the point that you don't reason so much about the language's performance, as about your compiler's optimization, seems to be extremely important in Haskell - more so than in any other language I'm familiar with. I'm inclined to say that the importance of the compiler is the factor that differentiates reasoning about performance in lazy, functional langauges, from reasoning about the performance of any other type. Addendum: Anyone happening on this question may want to look at the slides from Johan Tibell's talk about high performance Haskell.