Wednesday 30 January 2013

Which is quicker? Iteration or Recursion?

For a simple problem, see Project Euler #1, I thought it would an interesting exercise to code alternative implementations and time them. You see, when I originally solved the problem way back in the mists of time (well, perhaps not that long ago as it was done using F# 1.9.9 as far as I recall) I was using PE as a tool to teach myself F#. Nowadays I'm comfortable with F# so I thought I would return to PE and explore a little more. So, follows is some F# code which solves problem #1. Spoiler alert! If you haven't already solved it and intend to do so, stop reading now!
 let first = fun (a,b) -> a
  
 let second = fun (a,b) -> b
  
 let upperLimit = 1000 // ie we want to act upon the integers which are less than 1000
  
 let maxLoops = 25000 // an arbitrary large integer - the number of times to repeat the core calculation in a single (timed) execution
  
 let howMany = 15 // ie the number of times we want to perform the calculation in order to find the average calculation time
  
 let st = new System.Diagnostics.Stopwatch()
  
 let Impl1a = fun () ->
  
   let whichImpl = "Iteration"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let mutable sum = 0    
  
     for i = 1 to upperLimit-1 do
  
       sum <- if (i%3 = 0 || i%5 = 0) then sum + i else sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl1b = fun () ->
  
   let whichImpl = "Iteration with mutable creation outside loop"
  
   st.Restart()
  
   let mutable sum = 0    
  
   for loop = 1 to maxLoops do
  
     sum <- 0
  
     for i = 1 to upperLimit-1 do
  
       sum <- if (i%3 = 0 || i%5 = 0) then sum + i else sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl1c = fun () ->
  
   let whichImpl = "Reverse iteration"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let mutable sum = 0    
  
     for i = upperLimit-1 downto 1 do
  
       sum <- if (i%3 = 0 || i%5 = 0) then sum + i else sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl1d = fun () ->
  
   let whichImpl = "Reverse iteration with mutable creation outside loop"
  
   st.Restart()
  
   let mutable sum = 0    
  
   for loop = 1 to maxLoops do
  
     sum <- 0
  
     for i = upperLimit-1 downto 1 do
  
       sum <- if (i%3 = 0 || i%5 = 0) then sum + i else sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2a = fun () ->
  
   let whichImpl = "Iteration using List higher-order fns"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = [1..upperLimit-1] |> List.filter (fun i -> i%3=0 || i%5=0) |> List.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2b = fun () ->
  
   let whichImpl = "Iteration using List higher-order fns with pre-compiled integer range"
  
   st.Restart()
  
   let input = [1..upperLimit-1]
  
   for loop = 1 to maxLoops do
  
     let sum = input |> List.filter (fun i -> i%3=0 || i%5=0) |> List.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2c = fun () ->
  
   let whichImpl = "Iteration using Seq higher-order fns"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = seq {1 .. upperLimit-1} |> Seq.filter (fun i -> i%3=0 || i%5=0) |> Seq.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2d = fun () ->
  
   let whichImpl = "Reverse Iteration using List higher-order fns"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = [upperLimit-1 .. -1 .. 1] |> List.filter (fun i -> i%3=0 || i%5=0) |> List.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2e = fun () ->
  
   let whichImpl = "Reverse Iteration using List higher-order fns with pre-compiled integer range"
  
   st.Restart()
  
   let input = [upperLimit-1 .. -1 .. 1]
  
   for loop = 1 to maxLoops do
  
     let sum = input |> List.filter (fun i -> i%3=0 || i%5=0) |> List.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl2f = fun () ->
  
   let whichImpl = "Reverse Iteration using Seq higher-order fns"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = seq {upperLimit-1 .. -1 .. 1} |> Seq.filter (fun i -> i%3=0 || i%5=0) |> Seq.fold (fun state i -> i+state) 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl3a = fun () ->
  
   let whichImpl = "Recursion with Accumulator"
  
   let rec Impl3p counter acc =
  
     if counter=upperLimit then
  
       acc
  
     else if counter%3=0 || counter%5=0 then
  
       Impl3p (counter+1) (acc+counter)
  
     else
  
       Impl3p (counter+1) acc
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = Impl3p 1 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl3b = fun () ->
  
   let whichImpl = "Recursion"
  
   let rec Impl3p counter =
  
     if counter=upperLimit then
  
       0
  
     else if counter%3=0 || counter%5=0 then
  
       counter + Impl3p (counter+1)
  
     else
  
       Impl3p (counter+1)
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = Impl3p 1
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl4a = fun () ->
  
   let whichImpl = "Iteration incrementing a pair of counters"
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let mutable sum = 0
  
     let mutable loopDiv3,loopDiv5 = 0,0
  
     while loopDiv3<upperLimit do // because loopDiv3 increases slower than loopDiv5
  
       loopDiv3 <- loopDiv3 + 3
  
       loopDiv5 <- loopDiv5 + 5      
  
       // if we haven't already added it in loopDiv5 ...
  
       sum <- sum + (if loopDiv3 < upperLimit then (if loopDiv3 % 5 <> 0 then loopDiv3 else 0) else 0)
  
       sum <- sum + (if loopDiv5 < upperLimit then loopDiv5 else 0)
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl4b = fun () ->
  
   let whichImpl = "Recursion using a pair of counters"
  
   let rec Impl4p counterDiv3 counterDiv5 =
  
     if counterDiv3>=upperLimit then
  
       0
  
     else 
  
       (if counterDiv3 % 5 <> 0 then counterDiv3 else 0) + (if counterDiv5 < upperLimit then counterDiv5 else 0) + Impl4p (counterDiv3+3) (counterDiv5+5)
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = Impl4p 0 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl
  
 let Impl4c = fun () ->
  
   let whichImpl = "Recursion with Accumulator using a pair of counters"
  
   let rec Impl4p counterDiv3 counterDiv5 acc =
  
     if counterDiv3>=upperLimit then
  
       acc
  
     else 
  
       Impl4p (counterDiv3+3) (counterDiv5+5) ((if counterDiv3 % 5 <> 0 then counterDiv3 else 0) + (if counterDiv5 < upperLimit then counterDiv5 else 0) + acc)
  
   st.Restart()
  
   for loop = 1 to maxLoops do
  
     let sum = Impl4p 0 0 0
  
     ignore sum
  
   st.Stop()
  
   st.ElapsedMilliseconds, whichImpl  

 let run (whichImpl : unit -> int64 * string) = 
  
   let res = [ for count = 1 to howMany do yield whichImpl() ]
  
   let name = res |> List.head |> second
  
   let elapsed = res |> List.map first
  
   elapsed |> List.iter (fun elem -> printfn "%s : %i iterations took %i milliseconds" name maxLoops elem)
  
   let avg = float (elapsed |> List.fold (fun state i -> state+i) 0L) / float (List.length elapsed)
  
   name, avg
  
 let implementations = [ Impl1a; Impl1b; Impl1c; Impl1d; Impl2a; Impl2b; Impl2c; Impl2d; Impl2e; Impl2f; Impl3a; Impl3b; Impl4a; Impl4b; Impl4c ]
  
 implementations |> List.map (fun impl -> run impl)
  
Well, well! How interesting! Using F# interactive, these are the results obtained from my test run:
  [("Iteration", 252.8);
  
   ("Iteration with mutable creation outside loop", 255.0666667);
  
   ("Reverse iteration", 225.0);
  
   ("Reverse iteration with mutable creation outside loop", 236.1333333);
  
   ("Iteration using List higher-order fns", 3509.933333);
  
   ("Iteration using List higher-order fns with pre-compiled integer range", 386.6);
  
   ("Iteration using Seq higher-order fns", 4848.933333);
  
   ("Reverse Iteration using List higher-order fns", 3679.466667);
  
   ("Reverse Iteration using List higher-order fns with pre-compiled integer range", 396.7333333);
  
   ("Reverse Iteration using Seq higher-order fns", 4883.266667);
  
   ("Recursion", 242.4);
  
   ("Recursion with Accumulator", 231.9333333);
  
   ("Iteration incrementing a pair of counters", 48.6);
  
   ("Recursion using a pair of counters", 90.4);
  
   ("Recursion with Accumulator using a pair of counters", 53.26666667)]  

No comments:

Post a Comment