Euler Project Problem 14 in F# - An exercise in optimization
The coding “coding-experiments” blog presents two alterative solutions to the “Euler Project Problem 14” (http://projecteuler.net/print.php?id=14):
http://coding-experiments.blogspot.com/2008/03/projecteulernet-problem-14-csharp-vs.html
His F# solution is around 55 times slower in than the C# version, the implication being that while F# is elegant, it’s also slow. My problem with this conjecture is that that the implementation of the F# version is considerably different to the implementation of the C# version. So lets have a look at the two different version, first the F#:
let seq_length n =
n |>
Seq.unfold(fun x ->
match x with
| x when x = 0L -> None
| x when x = 1L -> Some((1L,0L))
| x when x%2L=0L -> Some((x,x/2L))
| _ -> Some((x,3L*x + 1L))
) |> Seq.length
[for i in 1L..1000000L -> (i, seq_length i)]
|> List.fold1_left(fun (ai,al) (i,l) -> if l > al then (i,l) else (ai,al) )
|> (fun (x,y) -> x ) |> print_any
Then the C#:
class Euler14 {
static long CountTerms(long x, ref long[] terms) {
long tc = 1;
long k = x;
while (k != 1) {
tc++;
k = (k % 2 == 0) ? k / 2 : 3 * k + 1;
if (k <= x) {
if (terms[k] > 0) {
terms[x] = terms[k] + tc;
return terms[x];
}
}
}
terms[x] = tc;
return terms[x];
}
static long LongestSeq() {
long max = 1000000;
long[] termcount = new long[max + 1];
long tcmax = 0;
long c = 0;
long ix = 0;
for (int i = 1; i <= max; i++) {
c = CountTerms(i, ref termcount); if (c > tcmax) {
tcmax = c;
ix = i;
}
}
return ix;
}
static void Main(string[] args) {
Console.WriteLine(LongestSeq());
}
}
The C# executes in around 0.7 second on my machine, while the F# as given takes a whopping 29 seconds. The first thing to notice about the F# implementation is that we calculate and store the entire sequence then we calculate its length by walking the sequence again. Since we’re not interested in the sequence – just its length we can cut the time down by calculating the length straight off, this cuts us down to about 8 seconds:
let rec seq_length x n =
match x with
| x when x = 0L -> (n + 1)
| x when x = 1L -> seq_length 0L (n + 1)
| x when x%2L=0L ->seq_length (x/2L) (n + 1)
| _ -> seq_length (3L*x + 1L) (n + 1)
let rec loop i imax n =
let n' = seq_length i 0
let imax, n = if n' > n then i, n' else imax, n
if i < 1000000L then loop (i + 1L) imax n else imax
print_any (loop 1L 0L 0)
So even though that’s a big improvement, still no cigar. If we look more closely at the C# version, we see it plays a clever trick, it uses an array to memorise the lengths of sequences, so once we find a number we’ve already calculated we’ve no need to calculate it again. The simplest way to implement memorization is to use a System.Collections.Generic.Dicrionary:
#light
open System.Collections.Generic
let dict = new Dictionary< int64, int >()
let seq_length x n =
let updateDict n =
dict.Add(x, n)
n
let rec seq_length x n =
if dict.ContainsKey(x) then
updateDict (n + dict.[x])
else
match x with
| x when x = 0L -> updateDict (n + 1)
| x when x = 1L -> seq_length 0L (n + 1)
| x when x%2L=0L ->seq_length (x/2L) (n + 1)
| _ -> seq_length (3L*x + 1L) (n + 1)
seq_length x n
let rec loop i imax n =
let n'= seq_length i 0
let imax, n = if n' > n then i, n' else imax, n
if i < 1000000L then loop (i + 1L) imax n else imax
print_any (loop 1L 0L 0)
This takes us down to around 2.2 seconds – better but still a little way off the C# version. A bit educated guess work leads us to think this version is probably slower because, a) we used a dictionary instead of an array (although much move convent to use), b) we access tuples inside a loop cause objects to be created and we need to access properties (call methods) to retrieve the values. Cutting this out we arrive at an algorithm that is pretty much the same as the C#:
#light
let transform n = if n%2L = 0L then n/2L else n*3L+1L
let rec seq_length x k n memo =
if k = 1L then
memo.(x) <- (n+1)
(n+1)
else if k < 1000000L then
let foo = memo.[int k]
if foo > 0 then
memo.(x) <- foo + n
foo + n
else
seq_length x (transform k) (n+1) memo
else seq_length x (transform k) (n+1) memo
let rec loop i imax n memo =
let n' = seq_length i (int64 i) 0 memo
if i < 1000000 then
if n' > n then loop (i+1) i n' memo else loop (i+1) imax n memo
else imax
print_any (loop 1 1 0 (Array.create 1000001 0))
This executes in around 1 second on my machine. If we add the compile flags “--standalone -O3” we see the execution time go down to 0.85 seconds, I feel it may be possible to get closer that this but I have run out of ideas (for now). So while 0.15 seconds is significantly slower that the C# version (~21%) it’s pretty acceptable margin, and the advantages we are still considerable shorter, 23 lines versus ~60 lines.
So there you go, I think we can still safely claim that F# has a performance profile similar to C#. Having we lost some of our elegance by using arrays for memorisation, well probably, but for my money we’re still a lot more elegant and understandable that the C# version.
Thanks to Lars Nilsson, who gave me plenty of ideas to help with the optimization!Feedback:
Feedback was imported from my only blog engine, it's no longer possible to post feedback here.
How not to reverse a list - Chris Smith's completely unique view
Brian McNamara, the latest addition to our stellar F# dev team, wrote up a great blog post on some pitfalls
ocaml vs. F# for big integer &#8230; surprising performance test! &laquo; khi&#8217;s life with an ech - http://khigia.wordpress.com/2008/03/30/ocaml-vs-f-for-big-integer-surprising-performance-test/
ocaml vs. F# for big integer &#8230; surprising performance test! &laquo; khi&#8217;s life with an ech