Parsing F#ref可变变量与对象字段
我正在用F#编写一个解析器,它需要尽可能快(我希望在不到一分钟的时间内解析一个100 MB的文件)。正常情况下,它使用可变变量来存储下一个可用字符和下一个可用令牌(即lexer和解析器都正确地使用一个前瞻单元) 我当前的部分实现使用局部变量来实现这些。由于闭包变量不能是可变的(有人知道原因吗?),我将它们声明为ref:Parsing F#ref可变变量与对象字段,parsing,f#,closures,Parsing,F#,Closures,我正在用F#编写一个解析器,它需要尽可能快(我希望在不到一分钟的时间内解析一个100 MB的文件)。正常情况下,它使用可变变量来存储下一个可用字符和下一个可用令牌(即lexer和解析器都正确地使用一个前瞻单元) 我当前的部分实现使用局部变量来实现这些。由于闭包变量不能是可变的(有人知道原因吗?),我将它们声明为ref: let rec read file includepath = let c = ref ' ' let k = ref NONE let sb = new
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
我假设这有一些开销(我知道,不多,但我在这里尝试最大速度),这有点不雅观。最明显的替代方法是创建一个解析器类对象,并将可变变量设置为其中的字段。有人知道哪一个可能更快吗?对于哪种风格更好/更惯用,有没有共识?我是否还缺少另一个选项?我进行了以下分析:
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")
let test()=
tic()
设可变a=0.0
对于i=1到10 do
对于j=1至10000000 do
a您提到闭包无法捕获局部可变值,因此需要使用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...
我不太确定您实际上是如何使用readc
,但这可能是一个需要考虑的问题。此外,如果您仅将其声明为帮助程序闭包,则可能可以在不使用闭包的情况下重写代码(或使用尾部递归显式编写代码,将其转换为带有可变变量的命令式循环),以避免任何分配。您是否认为此任务可能是I/O绑定的,而不是计算绑定的?如果它是I/O绑定的,那么使用refs或mutables并不重要,重要的是使用异步编程模型加载数据。异步加载数据块并进行处理可以允许您在处理文件的早期部分时加载数据。在进行这种异步编程时,异步工作流是一个很大的帮助。如果您在将文本转换为AST时只需要1.7 Mb/秒,您可以使用任何可以找到的解析器。一些OCaml解析器组合器在解析大型且不太简单的语法时显示>30Mb/s。FParsec很好,你可以很容易地用它动态更改语法。Robert,根据我的经验,解析往往是CPU受限的,但你提出了一个很好的观点,如果你能让它成为I/O受限的,异步处理是值得研究的。ssp,每秒30 MB解析一个复杂的语法无疑是一个令人印象深刻的数字。我可以看看FParsec使用的技术。谢谢啊!!我刚刚查看了生成的代码,我看到了发生了什么,内部函数使用的外部变量作为额外参数传递——这在所有情况下都是正确的,这就是为什么可变变量必须是ref。所以最有效的解决方案可能是为不可变的函数保留闭包方法,但是,将可变变量设置为全局变量,这样就不会分配堆。(read
是整个解析器函数,其他所有函数都是它的内部函数,所以stream
在整个工作完成后都会被处理。)谢谢!
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...
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!