阅读开始F# - 罗伯特皮克林我专注于以下段落:
来自OCaml背景的程序员应该小心 在F#中使用异常。由于CLR的架构, 抛出异常非常昂贵 - 相当贵一点 比在OCaml。如果您抛出很多异常,请分析您的代码 仔细确定性能成本是否值得。如果 成本太高,适当修改代码。
为什么,因为CLR,如果F#
而不是OCaml
,抛出异常会更加昂贵?在这种情况下,适当修改代码的最佳方法是什么?
答案 0 :(得分:12)
CLR中的异常非常丰富,并提供了大量细节。 Rico Mariani在CLR的the cost of exceptions上发布了一篇(陈旧但仍然相关)的帖子,详细介绍了其中的一些内容。
因此,在CLR中抛出异常的相对成本高于其他一些环境,包括OCaml。
在这种情况下,适当修改代码的最佳方法是什么?
如果期望在正常的非特殊情况下引发异常,您可以以完全避免异常的方式重新考虑您的算法和API。例如,尝试提供一个替代API,您可以在引发异常之前测试环境。
答案 1 :(得分:8)
为什么,因为CLR,如果F#比在OCaml中抛出异常更昂贵?
OCaml针对使用异常作为控制流进行了大量优化。相比之下,.NET中的异常根本没有得到优化。
请注意,性能差异 huge 。 OCaml的例外情况比F#快600倍左右。根据我的基准测试,即使C ++在这方面也比OCaml快6倍左右。
尽管.NET异常据称提供了更多(OCaml提供了堆栈跟踪,你还想要什么?)我想不出有什么理由为什么它们应该像它们一样慢。
在这种情况下,适当修改代码的最佳方法是什么?
在F#中,您应该编写“全部”功能。这意味着你的函数应该返回一个表示结果类型的union类型的值,例如:正常或特殊。
特别是,对find
的调用应替换为tryFind
的调用,该调用返回option
类型的Some
类型的值或None
如果关键元素不在集合中。
答案 2 :(得分:7)
Reed已经解释了为什么.NET异常的行为与OCaml异常不同。通常,.NET异常仅适用于异常情况,并且是为此目的而设计的。 OCaml具有更轻量级的模型,因此它们也用于实现一些控制流模式。
举一个具体的例子,在OCaml中你可以使用异常来实现循环的中断。例如,假设您有一个函数test
来测试数字是否是我们想要的数字。以下迭代从1到100的数字并返回第一个匹配的数字:
// Simple exception used to return the result
exception Returned of int
try
// Iterate over numbers and throw if we find matching number
for n in 0 .. 100 do
printfn "Testing: %d" n
if test n then raise (Returned n)
-1 // Return -1 if not found
with Returned r -> r // Return the result here
要实现这一点,没有例外,您有两个选择。您可以编写具有相同行为的递归函数 - 如果您调用find 0
(并且它被编译为与在C#中return n
循环内使用for
基本相同的IL代码:< / p>
let rec find n =
printfn "Testing: %d" n
if n > 100 then -1 // Return -1 if not found
elif test n then n // Return the first found result
else find (n + 1) // Continue iterating
使用递归函数的编码可能有点长,但您也可以使用F#库提供的标准函数。这通常是重写将使用OCaml异常用于控制流的代码的最佳方法。在这种情况下,您可以写:
// Find the first value matching the 'test' predicate
let res = seq { 0 .. 100 } |> Seq.tryFind test
// This returns an option type which is 'None' if the value
// was not found and 'Some(x)' if the value was found.
// You can use pattern matching to return '-1' in the default case:
match res with
| None -> -1
| Some n -> n
如果您不熟悉选项类型,请查看一些介绍性材料。 F# wikibook has a good tutorial和MSDN documentation has useful examples。
使用Seq
模块中的适当函数通常会使代码更短,因此它更受欢迎。它可能比直接使用递归效率稍低,但在大多数情况下,您无需担心这一点。
编辑:我对实际表现感到好奇。如果输入是延迟生成的序列Seq.tryFind
而不是列表seq { 1 .. 100 }
(由于列表分配的成本),使用[ 1 .. 100 ]
的版本会更有效。通过这些更改以及返回第25个元素的test
函数,在我的计算机上运行代码100000次所需的时间是:
exceptions 2.400sec
recursion 0.013sec
Seq.tryFind 0.240sec
这是非常简单的示例,因此我认为使用Seq
的解决方案通常不会比使用递归编写的等效代码慢10倍。减速可能是由于额外数据结构的分配(表示序列,闭包的对象......)以及由于额外的间接(代码需要大量虚拟方法调用,而不仅仅是简单的数字操作和跳转)。但是,异常甚至更昂贵,并且不会使代码更短或更易读......