I have an F# function that returns a list of numbers starting from 0 in the pattern of skip n, choose n, skip n, choose n… up to a limit. For example, this function for input 2 will return [2, 3, 6, 7, 10, 11...].
Initially I implemented this as a non-tail-recursive function as below:
let rec indicesForStep start blockSize maxSize =
match start with
| i when i > maxSize -> []
| _ -> [for j in start .. ((min (start + blockSize) maxSize) - 1) -> j] @ indicesForStep (start + 2 * blockSize) blockSize maxSize
Thinking that tail recursion is desirable, I reimplemented it using an accumulator list as follows:
let indicesForStepTail start blockSize maxSize =
let rec indicesForStepInternal istart accumList =
match istart with
| i when i > maxSize -> accumList
| _ -> indicesForStepInternal (istart + 2 * blockSize) (accumList @ [for j in istart .. ((min (istart + blockSize) maxSize) - 1) -> j])
indicesForStepInternal start []
However, when I run this in fsi under Mono with the parameters 1, 1 and 20,000 (i.e. should return [1, 3, 5, 7...] up to 20,000), the tail-recursive version is significantly slower than the first version (12 seconds compared to sub-second).
Why is the tail-recursive version slower? Is it because of the list concatenation? Is it a compiler optimisation? Have I actually implemented it tail-recursively?
I also feel as if I should be using higher-order functions to do this, but I’m not sure exactly how to go about doing it.
As dave points out, the problem is that you’re using the
@operator to append lists. This is more significant performance issue than tail-recursion. In fact, tail-recursion doesn’t really speed-up the program too much (but it makes it work on large inputs where the stack would overflow).The reason why you’r second version is slower is that you’re appending shorter list (the one generated using
[...]) to a longer list (accumList). This is slower than appending longer list to a shorter list (because the operation needs to copy the first list).You can fix it by collecting the elements in the accumulator in a reversed order and then reversing it before returning the result:
As you can see, this has the shorter list (generated using
[...]) as the first argument to@and on my machine, it has similar performance to the non-tail-recursive version. Note that the[ ... ]comprehension generates elements in the reversed order – so that they can be reversed back at the end.You can also write the whole thing more nicely using the F#
seq { .. }syntax. You can avoid using the@operator completely, because it allows you to yield individual elemetns usingyieldand perform tail-recursive calls usingyield!:This is how I’d write it. When calling it, you just need to add
Seq.toListto evaluate the whole lazy sequence. The performance of this version is similar to the first one.EDIT With the correction from Daniel, the
Seqversion is actually slightly faster!