我正在用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
我认为这有一些开销(不多,我知道,但我在这里尝试最大速度),而且它有点不优雅。最明显的替代方法是创建一个解析器类对象,并将可变变量作为其中的字段。有谁知道哪个可能更快?是否有任何共识被认为是更好/更惯用的风格?我还缺少另一种选择吗?
答案 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的运行方式大致相同。如果您在托管堆中担心过多的变量,那么您将不再使用数组。