F#ref-mutable vars vs object fields

时间:2010-05-31 09:29:46

标签: parsing f# closures

我正在用F#编写一个解析器,它需要尽可能快(我希望在不到一分钟的时间内解析一个100 MB的文件)。正常情况下,它使用可变变量来存储下一个可用字符和下一个可用标记(即词法分析器和解析器正确使用一个前瞻单元)。

我当前的部分实现使用局部变量。因为闭包变量不可变(任何人都知道这个的原因吗?)我已经将它们声明为ref:

let rec read file includepath =
    let c = ref ' '
    let k = ref NONE
    let sb = new StringBuilder()
    use stream = File.OpenText file

    let readc() =
        c := stream.Read() |> char
    // etc

我认为这有一些开销(不多,我知道,但我在这里尝试最大速度),而且它有点不优雅。最明显的替代方法是创建一个解析器类对象,并将可变变量作为其中的字段。有谁知道哪个可能更快?是否有任何共识被认为是更好/更惯用的风格?我还缺少另一种选择吗?

2 个答案:

答案 0 :(得分:5)

您提到闭包无法捕获本地可变值,因此您需要使用ref。原因是需要在堆上分配在闭包中捕获的可变值(因为闭包是在堆上分配的)。

F#强制您明确地写这个(使用ref)。在C#中,您可以“捕获可变变量”,但编译器会将其转换为场景后面的堆分配对象中的字段,因此无论如何它都将在堆上。

摘要是:如果要使用闭包,需要在堆上分配可变变量。

现在,关于您的代码 - 您的实现使用ref,它为您正在使用的每个可变变量创建一个小对象。另一种方法是创建一个具有多个可变字段的单个对象。使用记录,你可以写:

type ReadClosure = {
  mutable c : char
  mutable k : SomeType } // whatever type you use here

let rec read file includepath = 
  let state = { c = ' '; k = NONE } 
  // ... 
  let readc() = 
    state.c <- stream.Read() |> char 
    // etc...

这可能会更有效率,因为你要分配一个对象而不是几个对象,但我不认为差异会很明显。

您的代码还有一个令人困惑的事情 - stream值将在函数read返回后处理,因此对stream.Read的调用可能无效(如果您致电{ readc完成后{1}}。

read

我不太确定你是如何使用let rec read file includepath = let c = ref ' ' use stream = File.OpenText file let readc() = c := stream.Read() |> char readc let f = read a1 a2 f() // This would fail! 的,但这可能是一个需要考虑的问题。另外,如果你只是将它声明为一个辅助关闭,你可能会在没有闭包的情况下重写代码(或者使用尾递归显式编写它,它被转换为带有可变变量的命令循环)以避免任何分配。

答案 1 :(得分:4)

我做了以下分析:

let test() = 
    tic()
    let mutable a = 0.0
    for i=1 to 10 do
        for j=1 to 10000000 do
            a <- a + float j
    toc("mutable")
let test2() = 
    tic()
    let a = ref 0.0
    for i=1 to 10 do
        for j=1 to 10000000 do
            a := !a + float j
    toc("ref")

可变的平均值是50ms,而ref是600ms。性能差异是由于可变变量在堆栈中,而ref变量在托管堆中。

相对差异很大。但是,10 ^ 8次访问是一个很大的数字。总时间是可以接受的。所以不要过分担心ref变量的性能。请记住:

  

过早优化是其根源   万恶之物。

我的建议是,您首先完成解析器,然后考虑优化它。在实际运行程序之前,您不会知道底部的位置。关于F#的一个好处是它简洁的语法和功能风格很好地支持代码重构。代码完成后,优化代码会很方便。 Here是一个剖析例子。

再一个例子,我们每天都使用.net数组,它也在托管堆中:

let test3() = 
    tic()
    let a = Array.create 1 0.0
    for i=1 to 10 do
        for j=1 to 10000000 do
            a.[0] <- a.[0] + float j
    toc("array")

test3()与ref的运行方式大致相同。如果您在托管堆中担心过多的变量,那么您将不再使用数组。