Seq.map比普通的for循环更快吗?

8
我正在学习F#,其中一个让我担忧的问题是性能。我编写了一个小型基准测试,比较了以同一语言编写的惯用的F#和命令式样式代码,在很大程度上使我惊讶的是,函数式版本运行得更快。
该基准测试包括:
1.使用File.ReadAllLines读取文本文件 2.反转每行中的字符顺序 3.使用File.WriteAllLines将结果写回到同一文件中。
以下是代码:
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

无论文件大小如何,“F#风格”的版本都可以在“C#风格”的版本时间的75%左右完成。我的问题是,为什么会这样?我没有看到命令式版本中明显的低效率。

1
向@Dr_Asik致敬,提出了一个准备充分的问题。 - Onorio Catenacci
2个回答

10

Seq.mapArray.map不同。因为序列(IEnumerable<T>)在枚举之前不会被求值,在F#风格的代码中,只有当File.WriteAllLines循环遍历由Seq.map生成的序列(而不是数组)时,才会实际进行任何计算。

换句话说,您的C#风格版本将所有字符串翻转并将翻转后的字符串存储在数组中,然后循环遍历该数组以写入文件。F#风格的版本将所有字符串翻转并直接写入文件。这意味着C#风格代码需要通过整个文件三次(读取到数组、构建反转数组、将数组写入文件)进行循环,而F#风格代码仅需要通过整个文件两次(读取到数组、将反转行写入文件)进行循环。

如果您使用File.ReadLines而不是File.ReadAllLines结合Seq.map,则可以获得最佳性能-但是您的输出文件必须与输入文件不同,因为您在读取输入时仍然要写入输出。


1
啊,我现在明白了 - C# 版本调用 File.WriteAllLines(string, string[]),而 F# 版本调用 File.WriteAllLines(string, IEnumerable<string>)。因此,确实只有 2 个循环而不是 3 个。我没有想到这个方法还有其他的重载。感谢您的解释! - Asik

1

Seq.map形式相对于常规循环有几个优点。它可以预先计算函数引用,只需一次;它可以避免变量赋值;并且可以使用输入序列长度来预设结果数组的大小。


1
这些看起来是非常有价值的观点,但我很难理解你的意思。你能否详细阐述并且举例说明每个观点吗?谢谢。 - Asik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接