当我比较IL
F# 为表达式生成的代码seq{}
与为用户定义的计算工作流生成的代码时,很明显它们seq{}
的实现方式非常不同:它生成的状态机类似于 C# 用于其迭代器方法的一次。另一方面,用户定义的工作流使用您所期望的相应的构建器对象。
所以我想知道 - 为什么有区别?
这是出于历史原因吗,例如“seq 在工作流之前就存在”?
或者,是否可以获得显着的性能?
还有什么原因?
当我比较IL
F# 为表达式生成的代码seq{}
与为用户定义的计算工作流生成的代码时,很明显它们seq{}
的实现方式非常不同:它生成的状态机类似于 C# 用于其迭代器方法的一次。另一方面,用户定义的工作流使用您所期望的相应的构建器对象。
所以我想知道 - 为什么有区别?
这是出于历史原因吗,例如“seq 在工作流之前就存在”?
或者,是否可以获得显着的性能?
还有什么原因?
这是由 F# 编译器执行的优化。据我所知,它实际上是在后来实现的——F# 编译器首先具有列表推导,然后是通用版本的计算表达式(也用于seq { ... }
),但效率较低,因此在以后的一些版本中添加了优化。
主要原因是这消除了许多分配和间接。假设您有类似的东西:
seq { for i in input do
yield i
yield i * 10 }
使用计算表达式时,这会被转换为:
seq.Delay(fun () -> seq.For(input, fun i ->
seq.Combine(seq.Yield(i), seq.Delay(fun () -> seq.Yield(i * 10)))))
有几个函数分配,For
循环总是需要调用 lambda 函数。优化把它变成了一个状态机(类似于 C# 状态机),所以MoveNext()
对生成的枚举器的操作只是改变了类的一些状态,然后返回......
您可以通过为序列定义自定义计算构建器来轻松比较性能:
type MSeqBuilder() =
member x.For(en, f) = Seq.collect f en
member x.Yield(v) = Seq.singleton v
member x.Delay(f) = Seq.delay f
member x.Combine(a, b) = Seq.concat [a; b]
let mseq = MSeqBuilder()
let input = [| 1 .. 100 |]
现在我们可以对此进行测试(#time
在 F# 交互中使用):
for i in 0 .. 10000 do
mseq { for x in input do
yield x
yield x * 10 }
|> Seq.length |> ignore
在我的电脑上,使用自定义生成mseq
器需要 2.644 秒,但使用内置优化seq
表达式时只需 0.065 秒。因此优化使序列表达式的效率显着提高。
从历史上看,计算表达式(“工作流程”)是序列表达式的概括:http: //blogs.msdn.com/b/dsyme/archive/2007/09/22/some-details-on-f-computation-expressions-又名 monadic-or-workflow-syntax.aspx。
但是,答案肯定是可以获得显着的性能。我无法打开任何可靠的链接(尽管在http://blogs.msdn.com/b/dsyme/archive/2007/11/30中提到了“与序列表达式中的‘when’过滤器相关的优化” /full-release-notes-for-f-1-9-3-7.aspx),但我确实记得这是在某个时间点出现的优化。我想说它的好处是不言而喻的:序列表达式是一种“核心”语言特性,值得进行任何优化。
同样,您会看到某些尾递归函数将被优化为循环,而不是尾调用。