13

阅读开始 F# - Robert Pickering我专注于以下段落:

来自 OCaml 背景的程序员在 F# 中使用异常时应该小心。由于 CLR 的架构,抛出异常非常昂贵——比在 OCaml 中要昂贵得多。如果您抛出大量异常,请仔细分析您的代码以确定性能成本是否值得。如果成本太高,请适当修改代码。

为什么,因为 CLR,抛出异常F#比 in更昂贵OCaml?在这种情况下,适当修改代码的最佳方法是什么?

4

3 回答 3

12

CLR 中的异常非常丰富,并且提供了很多细节。Rico Mariani在 CLR 中发布了一篇(旧的但仍然相关的)关于异常成本的帖子,其中详细介绍了其中的一些内容。

因此,在 CLR 中引发异常比在其他一些环境(包括 OCaml)中具有更高的相对成本。

在这种情况下,适当修改代码的最佳方法是什么?

如果您希望在正常、非异常情况下引发异常,您可以重新考虑您的算法和 API,以完全避免异常。例如,尝试提供一个替代 API,您可以在其中测试环境,然后再引发异常。

于 2012-06-09T21:56:24.860 回答
8

为什么,因为 CLR,如果 F# 抛出异常比在 OCaml 中更昂贵?

OCaml 针对使用异常作为控制流进行了高度优化。相反,.NET 中的异常根本没有得到优化。

请注意,性能差异是巨大的。OCaml 中的异常比 F# 中的异常快大约 600 倍。根据我的基准测试,在这方面甚至 C++ 也比 OCaml 慢 6 倍左右。

尽管 .NET 异常据称提供了更多(OCaml 提供了堆栈跟踪,你还想要什么?)我想不出任何理由为什么它们应该像现在这样慢。

在这种情况下,适当修改代码的最佳方法是什么?

在 F# 中,您应该编写“总计”函数。这意味着您的函数应该返回一个联合类型的值,指示结果的种类,例如正常或异常。

特别是,find应将调用替换为tryFind返回option类型为Some值或None键元素不存在于集合中的值的调用。

于 2012-06-10T18:35:44.330 回答
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

要毫无例外地实现这一点,您有两个选择。您可以编写一个具有相同行为的递归函数 - 如果您调用(并且它被编译为与在 C# 中使用内部循环find 0基本相同的 IL 代码):return nfor

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 有很好的教程MSDN 文档也有有用的示例

使用Seq模块中的适当函数通常会使代码更短,因此是可取的。它可能比直接使用递归效率略低,但在大多数情况下,您不必担心这一点。

编辑:我对实际表现很好奇。如果输入是延迟生成的序列而不是列表(因为列表分配的成本),则使用版本Seq.tryFind会更有效。通过这些更改以及返回第 25 个元素的函数,在我的机器上运行代码 100000 次所需的时间是:seq { 1 .. 100 }[ 1 .. 100 ]test

exceptions   2.400sec  
recursion    0.013sec  
Seq.tryFind  0.240sec

这是非常微不足道的示例,因此我认为使用的解决方案Seq通常不会比使用递归编写的等效代码慢 10 倍。速度变慢可能是由于分配了额外的数据结构(表示序列的对象、闭包......)以及额外的间接性(代码需要大量的虚拟方法调用,而不仅仅是简单的数字操作和跳转)。但是,异常的成本更高,并且不会以任何方式使代码更短或更易读……

于 2012-06-09T22:25:55.150 回答