I’m learning F# and one thing that preoccupies me about this language is performance. I’ve written a small benchmark where I compare idiomatic F# to imperative-style code written in the same language – and much to my surprise, the functional version comes out significantly faster.
The benchmark consists of:
- Reading in a text file using File.ReadAllLines
- Reversing the order of characters within each line
- Writing back the result to the same file using File.WriteAllLines.
Here’s the code:
open System
open System.IO
open System.Diagnostics
let reverseString(str:string) =
new string(Array.rev(str.ToCharArray()))
let CSharpStyle() =
let lines = File.ReadAllLines("text.txt")
for i in 0 .. lines.Length - 1 do
lines.[i] <- reverseString(lines.[i])
File.WriteAllLines("text.txt", lines)
let FSharpStyle() =
File.ReadAllLines("text.txt")
|> Seq.map reverseString
|> (fun lines -> File.WriteAllLines("text.txt", lines))
let benchmark func message =
// initial call for warm-up
func()
let sw = Stopwatch.StartNew()
for i in 0 .. 19 do
func()
printfn message sw.ElapsedMilliseconds
[<EntryPoint>]
let main args =
benchmark CSharpStyle "C# time: %d ms"
benchmark FSharpStyle "F# time: %d ms"
0
Whatever the size of the file, the “F#-style” version completes in around 75% of the time of the “C#-style” version. My question is, why is that? I see no obvious inefficiency in the imperative version.
Seq.mapis different fromArray.map. Because sequences (IEnumerable<T>) are not evaluated until they are enumerated, in the F#-style code no computation actually happens untilFile.WriteAllLinesloops through the sequence (not array) generated bySeq.map.In other words, your C#-style version is reversing all the strings and storing the reversed strings in an array, and then looping through the array to write out to the file. The F#-style version is reversing all the strings and writing them more-or-less directly to the file. That means the C#-style code is looping through the entire file three times (read to array, build reversed array, write array to file), while the F#-style code is looping through the entire file only twice (read to array, write reversed lines to file).
You’d get the best performance of all if you used
File.ReadLinesinstead ofFile.ReadAllLinescombined withSeq.map– but your output file would have to be different from your input file, as you’d be writing to output while still reading from input.