9

Since F# 2.0 has become a part of VS2010 I take an interest in F#. I wondered what's the point of using it. I'd read a bit and I made a benchmark to measure functions calling. I have used Ackermann's function :)

C#

sealed class Program
{
    public static int ackermann(int m, int n)
    {
        if (m == 0)
            return n + 1;
        if (m > 0 && n == 0)
        {
            return ackermann(m - 1, 1);
        }
        if (m > 0 && n > 0)
        {
            return ackermann(m - 1, ackermann(m, n - 1));
        }
        return 0;
    }

    static void Main(string[] args)
    {

        Stopwatch stopWatch = new Stopwatch();

        stopWatch.Start();
        Console.WriteLine("C# ackermann(3,10) = " + Program.ackermann(3, 10));
        stopWatch.Stop();

        Console.WriteLine("Time required for execution: " + stopWatch.ElapsedMilliseconds + "ms");
        Console.ReadLine();
    }
}

C++

class Program{
public:
static inline int ackermann(int m, int n)
{
  if(m == 0)
       return n + 1;
  if (m > 0 && n == 0)
  {
      return ackermann(m - 1, 1);
  }
  if (m > 0 && n > 0)
  {
      return ackermann(m - 1, ackermann(m, n - 1));
  }
  return 0;
 }
};

int _tmain(int argc, _TCHAR* argv[])
{
 clock_t start, end;

  start = clock();
 std::cout << "CPP: ackermann(3,10) = " << Program::ackermann(3, 10) << std::endl;
 end = clock();
 std::cout << "Time required for execution: " << (end-start) << " ms." << "\n\n";
 int i;
 std::cin >> i;
 return 0;
}

F#

// Ackermann
let rec ackermann m n  =
  if m = 0 then n + 1
  elif m > 0 && n = 0 then ackermann (m - 1) 1
  elif m > 0 && n > 0 then ackermann (m - 1)  (ackermann m (n - 1))
  else 0

open System.Diagnostics;
let stopWatch = Stopwatch.StartNew()
let x = ackermann 3 10 
stopWatch.Stop();

printfn "F# ackermann(3,10) = %d"  x
printfn "Time required for execution: %f"  stopWatch.Elapsed.TotalMilliseconds

Java

public class Main 
{
 public static int ackermann(int m, int n)
 {
 if (m==0) 
   return n + 1;
if (m>0 && n==0)
{
 return ackermann(m - 1,1);
}
if (m>0 && n>0)
{
  return ackermann(m - 1,ackermann(m,n - 1));
 }
 return 0;
}

  public static void main(String[] args)
  { 
   System.out.println(Main.ackermann(3,10));
  }
}

An then
C# = 510ms
c++ = 130ms
F# = 185ms
Java = Stackoverflow :)

Is it the power of F# (except small amount of code) If we want to use .Net and gain a bit faster execution? Can I optimalize any of these codes (especially F#) ?

UPDATE. I got rid off Console.WriteLine and run the C# code without the debugger: C# = 400ms

Lukasz Madon
  • 14,664
  • 14
  • 64
  • 108
  • 9
    Execute your method once before running the benchmark. Your times have included the time taken to JIT the intermediate language. Also, take methods like Console.WriteLine() outside of your benchmark, because they're seriously slow. – Mark H Jul 13 '10 at 22:21
  • 1
    "Execute your method once before running the benchmark. Your times have included the time taken to JIT the intermediate language. " Is it? i add let dd = ackermann 3 10 and gives me 7 ms extra. for C# didnt change :) "Also, take methods like Console.WriteLine() outside of your benchmark, because they're seriously slow." Good idea but didnt speed up – Lukasz Madon Jul 13 '10 at 22:36
  • Yeah, F# and C# are both JIT compiled - run the method twice and use the second benchmark. The reason is that the JIT compiler optimizes the machine code for your processor's specific capabilities (CISC computing goodness) – Aaronontheweb Jul 14 '10 at 00:14
  • Why not include the first execution in benchmark? Can we really just ignore the JIT? Isn't this cheating? – Inverse Jul 14 '10 at 07:03
  • 1
    Not really. You can prejit the application, and if the app runs for an hour - the first method call will simply not be relevant at all. – TomTom Jul 14 '10 at 07:23
  • 1 - Got to ditch these prints - it's not a test of library writer's IQ deficit (Console.WriteLine is painful to watch just printing constants in a PowerShell - someone needs to get sued over that :-) 2 - Got to skip first 2-5 runs or include the time it takes to compile C++ (first run for JIT the rest to let GC calm down - so OK C++ wins anyway :-) 3 - Ackerman is well known as a stack-trashing test, how about something realistic like matrix inversion :-) 4 - Will someone please fix that Java - this is plain embarrassing :-))) – ZXX Aug 04 '10 at 12:47

4 Answers4

14

I believe that in this case, the difference between C# and F# is thanks to tail-call optimization.

A tail-call is when you have a recursive call that is done as "the last thing" in a function. In this case, F# doesn't actually generate a call instruction, but instead compiles the code into a loop (because the call is the "last thing", we can reuse the current stack frame and just jump to the beginning of the method).

In your implementation of ackermann, two of the calls are tail-calls and only one of them is not (the one where the result is passed as an argument to another ackermann call). This means that the F# version actually invokes a "call" instruction (much?) less often.

In generall, the performance profile is roughly the same as performance profile of C#. There are quite a few posts related to F# vs. C# performance here:

Community
  • 1
  • 1
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • 2
    I've been hand writing a recursive descent parser in C# and often I regret not using F# for a lack of tail call optimizations. – ChaosPandion Jul 13 '10 at 22:30
  • 7
    ChaosPandion: You're writing a parser in C# and the only reason you regret not using F# is that it might not optimize tail calls? Really? That's the only reason? – Gabe Jul 13 '10 at 23:20
  • @lukas: The VC++ compiler (hence optimizer) is much more mature than the C# compiler. Even C++/CLI code which runs on the CLR often performs better than the equivalent C#. Actually, the fact that the F# performance is so close to native C++ is a testament to the potential performance of CLR code. – Cogwheel Jul 13 '10 at 23:24
  • @lukas: I cannot help with C++ as I'm not really a C++ expert (but it certainly doesn't do tail-call optimizations). – Tomas Petricek Jul 13 '10 at 23:26
  • @Gabe - What are you trying to say? I mean I also like the syntax of F#. – ChaosPandion Jul 13 '10 at 23:42
  • ChaosPandion: I would regret not using F# for the lack of matching expressions! You could probably write the parser as just a single function if you're really clever... – Gabe Jul 14 '10 at 00:14
  • @Gabe - Hmmm, I do regret that as well. To be fair I didn't say that was my only regret. – ChaosPandion Jul 14 '10 at 00:32
  • 2
    Actually, some C++ compilers [do tail-call optimization](http://stackoverflow.com/questions/34125/which-if-any-c-compilers-do-tail-recursion-optimization), among them Visual C++. – wmeyer Mar 16 '11 at 21:04
6

This is kind of function calling related since it's a common method to reduce function calls.

You can make this type of recursive function go faster by way of memoization (caching). You can also do this in C# (example.) I got 18ms.

open System.Collections.Generic

let memoize2 f =
    let cache = Dictionary<_, _>()
    fun x y ->
        if cache.ContainsKey (x, y) then 
            cache.[(x, y)]
        else 
            let res = f x y
            cache.[(x, y)] <- res
            res

// Ackermann
let rec ackermann =
    memoize2 (fun m n ->
        if m = 0 then n + 1
        elif m > 0 && n = 0 then ackermann (m - 1) 1
        elif m > 0 && n > 0 then ackermann (m - 1)  (ackermann m (n - 1))
        else 0
    )
gradbot
  • 13,732
  • 5
  • 36
  • 69
4

Not directly related to the question, but interesting enough to mention: try this version

let rec ackermann2 m n  =
  match m,n with
  | 0,0 -> 0
  | 0,n -> n+1
  | m,0 -> ackermann2 (m-1) 1
  | m,n -> ackermann2 (m-1) (ackermann2 m (n-1))

On my PC (VS2010, F# interactive) it's almost 50% faster than your version when calculating ackermann 3 12.

I can't quite explain why there's such a performance difference. I guess it has something to do with the F# compiler translating the match expression to a switch statement instead of a series of if statements. Putting the (m=0,n=0) test first may also have helped.

cfern
  • 5,956
  • 2
  • 25
  • 22
  • 1
    The pattern match compiler should restructure the tests to minimize redundancies and the original code did the `m>0` test twice. – J D Jul 16 '10 at 03:52
1

For F# you may want to try the inline keyword.

Also, as the previous poster mentioned, the C# and F# versions differ due to the Console.WriteLine statements.

Fred
  • 31
  • 1