【问题标题】:CLR vs OCaml exception overheadCLR 与 OCaml 异常开销
【发布时间】:2012-06-13 10:59:08
【问题描述】:

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

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

为什么,由于 CLR,如果F# 引发异常比OCaml 更昂贵?在这种情况下,适当修改代码的最佳方法是什么?

【问题讨论】:

标签: exception f# clr ocaml


【解决方案1】:

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

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

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

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

【讨论】:

  • option 类型在这里非常重要。
  • @ildjarn 是的,完全正确。通常,您可以使用None 而不是使用异常。
  • Option 类型也类似,但比空引用更好。它是具有两个构造函数 Some a 和 None 的类型。基本上 None 是 Null 并且 Some a 是填充值。它比 null 更好,因为类型系统在您进行模式匹配以获取您的值时强制执行 None 处理,因此您永远不会在不知不觉中获得 null ref 异常。
  • “CLR 中的异常非常丰富”。我认为,如果您的回答能详细说明其中的一些丰富性,那就太好了。 OCaml 异常包含堆栈跟踪、辅助数据(例如错误消息)、可以重新引发等等。那么 .NET 究竟提供了什么来解释为什么它比 OCaml 慢 600 倍?
  • @JonHarrop - 有关 SEH 和 .NET 异常模型的一些复杂性的描述,请参阅 blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx。我怀疑在 SEH 上构建有一些好处(例如,在本机代码和托管代码之间编组异常,调试器能够打破第一次机会异常,支持 finally 块)。这些功能是否值得付出性能成本当然值得商榷。
【解决方案2】:

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

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

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

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

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

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

特别是,对find 的调用应替换为对tryFind 的调用,该调用返回option 类型的值,如果关键元素不存在于收藏。

【讨论】:

  • I cannot think of any reason why they should be as slow as they are. 这正是我要问的。 Reed Copsey 发布的链接The Cost of Exceptions 非常简单地解释了这种缓慢的原因。
  • @gliderkite 那篇文章指出 .NET 很慢并描述了解决方法(避免使用 .NET 的这一部分),但我认为它没有描述原因。我认为答案很简单,就是微软从不费心优化它。
  • I don't think it describes why 不幸的是它是这样,但我认为微软从来没有费心优化它,这样可以获得什么收益?
【解决方案3】:

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# 中的 for 循环中使用 return n 基本相同的 IL 代码):

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 tutorialMSDN documentation has useful examples 也是。

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

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

exceptions   2.400sec  
recursion    0.013sec  
Seq.tryFind  0.240sec

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

【讨论】:

  • +1 我一定要读你的书。只有一件事,为什么使用 Seq 中的函数可能比直接使用递归效率低?
  • @gliderkite 我运行了一个原始测试并添加了一些性能信息。
猜你喜欢
  • 2011-10-04
  • 1970-01-01
  • 2012-08-23
  • 2010-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多