CLR与OCaml异常开销

时间:2012-06-09 21:52:32

标签: exception f# clr ocaml

阅读开始F# - 罗伯特皮克林我专注于以下段落:

  

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

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

3 个答案:

答案 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 tutorialMSDN 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倍。减速可能是由于额外数据结构的分配(表示序列,闭包的对象......)以及由于额外的间接(代码需要大量虚拟方法调用,而不仅仅是简单的数字操作和跳转)。但是,异常甚至更昂贵,并且不会使代码更短或更易读......