-3

I was practicing a bit with Haskell to get to know the language (It's amazing) so I went to project euler and did problem #2 which took quite a while (~ 30-40s I don't know how long exactly) to finish. I wondered why it took so long so I tried the same with F#, C# Javascript and Python. F# and C# took a couple of ms to finish as well as javascript but python took more time than Haskell did. Why is that? These are my implementations

Haskell

fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

genFibs n maxVal = 
if fib n < maxVal then 
    fib(n):(genFibs (n+1) maxVal) 
else []

totalSum = sum (filter even (genFibs 1 4000000))

main = print totalSum

F#

let rec fib n = 
    match n with
    | n when n < 2 -> 1
    | n -> fib (n-1) + fib (n-2)

let rec genFibos n max = 
    match fib(n) < max with
    | true -> fib(n)::(genFibos (n + 1) max)
    | false -> []

genFibos 1 4000000
|> Seq.where (fun n -> n % 2 = 0)
|> Seq.sum

C#

static int Fib(int n)
{
    return n < 2 ? 1 : Fib(n - 1) + Fib(n - 2);
}

public static int EvenFibs()
{
    var n = 1;
    var totalSum = 0;
    var num = 1;
    while(num < 4000000)
    {
        num = Fib(n);
        if (num % 2 == 0) totalSum += num;
        n += 1;
    }
    return totalSum;
}

Python

knownVals = { }
def fib(n):
if n not in knownVals:
   knownVals[n] = 1 if n < 2 else fib(n-1) + fib(n-2)
return knownVals[n]

n = 1
stillCounting = True
totalSum = 0

while stillCounting: 
    num = fib(n)
    if num > 4000000:
        stillCounting = False
    else:
        if num % 2 == 0:
            totalSum += num
        n += 1

print(totalSum)

Javascript

(function () {

function fib(n) {
    return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}

var totalSum = 0;
var num = 1;
var n = 1;

while(num < 4000000)
{
    num = fib(n);
    if (num % 2 == 0) totalSum += num;
    n += 1;
}

alert(totalSum);

})();

So can someone explain why Haskell and Python were slow and the F# and C# were fast on this and how I can enhance it? Any help would be appreciated!

EDIT: Haskell code corrected, Python better implementation using memoization

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Zaid Ajaj
  • 680
  • 8
  • 16
  • 1
    The python is probably slow because python is slow in general. – Robert Harvey Dec 16 '14 at 18:40
  • The only way that code could take a couple ms on any system is if some compiler is recognizing naive Fibonacci and replacing it with a better algorithm. – Carl Dec 16 '14 at 18:42
  • @RobertHarvey Surely it cannot be slow because he is using an EXPTIME algorithm... My bet: F# and C# compilers are smart and introduced memoization on the recursive calls... – Bakuriu Dec 16 '14 at 18:43
  • 2
    @RobertHarvey Python isn't that slow. I use it 100% for large amounts of numeric processing and it's more than capable. The problem is the implementation, not the language. – bheklilr Dec 16 '14 at 18:45
  • 3
    BTW: your Haskell implementation is utterly wrong. It should be `fib n = fib (n-1) + fib (n-2)` **not** `(fib n - 1) + (fib n - 2)`. In fact your definition of `fib` diverges (i.e. it loops forever, or until a stackoverflow). – Bakuriu Dec 16 '14 at 18:47
  • @Bakuriu: His C# solution takes 275 ms to run on my machine (Intel 4790K 4Ghz). – Robert Harvey Dec 16 '14 at 18:50
  • 1
    The python solution runs in less than 10 seconds on my machine. The corrected Haskell solution runs in ~1 second. Something's not right here. – bheklilr Dec 16 '14 at 18:53
  • @Bakuriu I thought it didn't matter (for now I am just playing around with Haskell) – Zaid Ajaj Dec 16 '14 at 19:00
  • @bheklilr I think then it has to do with my specs :) – Zaid Ajaj Dec 16 '14 at 19:01
  • @ZaidAjaj You put the parenthsis in all the wrong places. Keep in mind that in haskell function call has highest priority: `fib n - 1 == (fib n) - 1` so you have to use `fib (n-1)`. – Bakuriu Dec 16 '14 at 19:03
  • 3
    @ZaidAjaj You should make sure that you've compiled it using `ghc -O2` to get optimizations as well. Running it using `runhaskell` or `GHCi` will result in significantly slower execution. Also, a faster implementation of this problem would be `sum $ filter even $ takeWhile (<= 4000000) fibs where fibs = 1 : 1 : zipWith (+) fibs (tail fibs)`. This builds an infinite list of numbers, takes only the ones you need, strips out all odd numbers, and sums them up all on one line. – bheklilr Dec 16 '14 at 19:04
  • @Bakuriu But it did work somehow and I got the expected result – Zaid Ajaj Dec 16 '14 at 19:18
  • Trying to run your haskell code I get: ``$./fibo2 Stack space overflow: current size 8388608 bytes. Use `+RTS -Ksize -RTS' to increase it. `` if you got the expected result it simply means you changed the code before posting. – Bakuriu Dec 16 '14 at 19:29
  • @Bakuriu Yes indeed it was (fib $ n - 1) + (fib $ n - 2) so yeah I did change it, thinking that it would work anyway! – Zaid Ajaj Dec 16 '14 at 19:47

4 Answers4

3

Your Haskell implementation is simply wrong: it never completes because:

fib n = (fib n - 1) + (fib n - 2)

is the same as:

fib n = (fib n) - 1 + ((fib n) - 2)

which diverges.

The correct implementation:

fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

genFibs n maxVal
  | fib n < maxVal = fib n : genFibs (n+1) maxVal
  | otherwise = []

totalSum = sum $ filter even $ genFibs 1 4000000

main = print totalSum

Runs in about 1 second on my machine:

$time ./fibo 
4613732

real    0m1.334s
user    0m1.324s
sys     0m0.009s

The problem with the python solution is that the interpreter isn't introducing any kind of optimization or memoization for the double recursive call. This means that the algorithm takes exponential time to compute the answer. It's pretty easy to come up with a polynomial time algorithm:

def fibo(n):
    prev, cur = 0, 1
    for i in range(n):
        prev, cur = cur, prev + cur
    return cur

Which results in:

In [8]: %%timeit
   ...: tot = 0
   ...: n = 0
   ...: while True:
   ...:     num = fibo(n)
   ...:     if num > 4000000:
   ...:         break
   ...:     elif num % 2 == 0:
   ...:         tot += num
   ...:     n += 1
   ...:     
10000 loops, best of 3: 54.8 µs per loop

My guess for the speed of F# and C# is that the compiler is introducing some form of memoization to avoid exponential growth. Although the problem may still be too small to notice the exponential growth in function calls. If you try to increase the 4000000 to 400 hundred millions or more, you can definitely check if this is true.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • On my machine, after introducing memoization, the F# solution takes 0.005 seconds. Python is only fast when the majority of the execution takes place in the C implementation without a lot of switching between the python and the C. – N_A Dec 16 '14 at 19:10
  • @mydogisbox Your comment simply prooves what I said: if introducing memoization doesn't change runtime it *does* mean that the compiler *is* introducing it already. There is no way that `k*33` calls takes about the same time as `k' * 2^32` calls (the number of calls performed by the naive algorithm without memoization). You can see the difference in the python implementation: from order of ten second to order of 10 *micro* seconds. Also, both implementation are pure python without calls to built-ins, so your point about switching to C is completely invalid (in *this* case) – Bakuriu Dec 16 '14 at 19:15
  • My point was that .net doesn't add momoization for you (although that would be pretty cool). – N_A Dec 16 '14 at 19:24
  • For some reason I didn't see your improved Python implementation. – N_A Dec 16 '14 at 19:27
  • As you iterate through the values, you could maintain a "previous-value" and "current-value" variable and totally avoid recursion. – Pointy Dec 16 '14 at 20:52
  • "The problem with the Python solution" is also a problem with the Haskell solution, so if you use bheklilr's comment using the classic fibs list:`sum $ filter even $ takeWhile (<= 4000000) fibs where fibs = 1 : 1 : zipWith (+) fibs (tail fibs)` you'll get a performance improvement. – AndrewC Dec 16 '14 at 21:11
1

try memoizing it

known_fibs={}
def fib(n):
    if n not in known_fibs:
        known_fibs[n] = 1 if n < 2 else fib(n-1) + fib(n-2)
    return known_fibs[n]
Joran Beasley
  • 110,522
  • 12
  • 160
  • 179
  • The Python solution he provides in his question is not that slow, unless he's got a really old computer. It runs in 3 seconds on my machine. – Robert Harvey Dec 16 '14 at 18:58
  • this worked nicely for python...I will try to figure out something similar for the other implementations – Zaid Ajaj Dec 16 '14 at 18:59
1

You want to find out where all the time is going, right? Don't think of it as measuring time. Think of it as finding lines of code that are on the stack a large fraction of the time, regardless of the total. Here's an example: How to improve performance of this code?

Many profilers fall into the gprof traps, including ignoring blocked time, thinking that lines of code don't matter, thinking that "self time" does, and thinking that measurement has to be accurate

Community
  • 1
  • 1
Évariste Galois
  • 1,043
  • 2
  • 13
  • 27
0

The recursive definition of the Fibonacci function is a terrible way to solve this problem. Here's a JavaScript implementation that runs essentially instantaneously:

function evenFibSum(limit) {
  var
    pf, f, t, sum;

  for (pf = 1, f = 2, sum = 0; f <= limit; t = f, f += pf, pf = t)
    if (!(f & 1)) sum += f;

  return sum;
}
console.log(evenFibSum(4000000));

Recursively generating each value as you work through the whole list is doing way too much work, and it's extremely simple to iteratively compute the values. Memoizing the recursive definition helps but it seems needlessly complicated to me.

Pointy
  • 405,095
  • 59
  • 585
  • 614