【问题标题】:OCaml performance of exceptionsOCaml 的异常表现
【发布时间】:2012-08-23 00:39:57
【问题描述】:

我经常读到异常有点慢,如果性能是一个问题(例如,在 Java、F# 等中),应该避免。这是否适用于常见的 OCaml 函数,例如 Hashtbl.find,它返回未找到元素的异常?

特别是,如果我希望我的应用程序高效,我是否应该在调用Hashtbl.find 之前始终使用Hashtable.mem 来测试元素成员资格?或者mem 函数的额外比较会对性能产生负面影响?

【问题讨论】:

    标签: performance f# ocaml


    【解决方案1】:

    Hashtbl.find 之前首先回答Hashtbl.mem 的具体问题: 不要这样做,因为检查表中元素是否存在必须两次;虽然两种策略的成本都是 O(1),但首先调用 Hashtbl.mem 将计算哈希值和查找两次——这可能比获取异常花费更长的时间。

    作为一般建议,仅当异常概率较低时才创建引发异常的函数。类型系统不考虑异常,因此更健壮的实现将具有 'a option 返回值而不是 'a 加上可能的异常。 ocaml 核心 库使用_exn 后缀来明确函数可能会抛出异常,但通常更喜欢非异常抛出的函数。

    因此,对于哈希表,您应该(在理想情况下)具有两个功能:

    Hashtbl.find_exn : 'a t -> key -> 'a (* throws Not_found *)

    Hashtbl.find : 'a t -> key -> 'a option

    如果有疑问,请使用第二个。如果不是为了纯粹的速度,您可以在原始哈希表周围编写一个简单的包装器:

    find_safe h k = try Some (Hashtbl.find h k) with Not_found -> None

    【讨论】:

    • 您的回答非常明智,但我强烈不同意“仅...如果异常概率低则引发异常”的想法。概率问题既不是由性能考虑(引发异常通常在性能方面很好)也不是由正确性考虑(低概率错误仍然是错误)保证。我不明白它在 OCaml 的上下文中来自哪里。
    • 你必须考虑局部性和捕获错误的代码:如果这是一个严重的错误,你可能想放弃你当前的计算,例如通知用户。如果您想摆脱一系列函数,这是非常乏味的,因为您必须在每个函数中集成错误处理 - 而不仅仅是围绕整个事物的 try/with 块。如果您通常预计某些事情会“出错”,您将在本地处理该错误(例如过滤结果)。您可能只在 OCaml 中看到过它,因为大多数语言都缺少临时选项类型,并且对异常的类型安全性较低。
    • 我不确定您的(合理的)评论与之前的“低概率”论点有何关联。您是否在“低概率”和“您无法在本地处理而应该向用户报告的错误”之间建立隐含联系?我同意在某些情况下放弃和报告是有用的,但我认为这与“错误概率”测量无关,而是与问题域的问题(谁有知识/有责任处理该问题)有关?)。具有模块化问题的单子风格的观点是(令人沮丧但)很好。
    【解决方案2】:

    OCaml 异常处理使引发和捕获异常的速度非常快——请参阅this SO thread 了解有关其实现方式的内部详细信息。我没有注意精确地对其进行基准测试,但我的随机猜测是它在间接函数调用的范围内。

    众所周知,与语言的其他部分相比,OCaml 异常比 F# 的异常要快得多——这给将代码从 OCaml 移植到 F# 的人们带来了性能问题。在 OCaml 中,异常不会导致性能问题。

    Hashtbl.find 之前调用Hashtbl.mem 可能比捕获异常要慢。惯用风格倾向于try Hashtbl.find .. with Not_found -> ...

    也就是说,OCaml 社区有一个明智的运动,即使用更明确的错误处理风格,使用option 类型而不是异常。基本原理不是基于性能,而是基于类型检查器阻止您忘记处理错误情况的事实。在设计全新的 API 时,我更愿意这样做。使用引发异常的第三方函数时,请确保立即捕获所有可能的异常;不这样做通常是一种设计味道,应该非常合理。

    由于方便,OCaml 异常也经常被用作纯控制流机制(而不是表示罕见的故障情况)。你会遇到这样的代码:

    try
      for i = 0 to .... do
        if .. then raise Exit
      done; false
    with Exit -> true
    

    最后,我觉得您可能在实施选择上采用了错误的方法。询问有关表演的一般微观问题通常不是要走的路。首先考虑正确性和可读性。性能问题通常应该稍后出现,并且仅在可衡量/可分析的情况下出现。

    【讨论】:

    • 实际上,在这种情况下,“标准 OCaml 方式”对我来说有点奇怪的原因之一是因为我读过 Effective Java,其中 Joshua Bloch 明确提到了在 Java 中应用的这样一个成语,并且应该避免它。我知道 OCaml 不是 Java(谢天谢地!),但它似乎是适用于大多数编程语言的通用技巧。因此,OCaml 几乎“强迫”你这样做的事实对我来说似乎是不自然的。无论如何,我同意优化是在更正之后进行的。我只是倾向于过度使用 OCaml 中的列表,因为它们简单而优雅,这导致我遇到了一些二次问题。
    • 异常是对流控制的优化:它是变相的非本地 goto 运算符。首先,正确进行流量控制很重要。异常在这里没有帮助,有时甚至有害(如果您不知道某些函数正在抛出异常)。当性能不够时,我们可以尝试通过放入一些异常来优化它。此外,我认为 Java 性能建议对 OCaml 没有帮助,因为实现细节大相径庭。
    • 我发现异常偶尔会有所帮助并增强代码的可读性(与其他情况下需要的管道相比,尤其是当库对单子样式的支持还没有完全实现时)。我不是在谈论跨函数或跨模块异常引发和捕获的一般情况,而是在捕获站点是静态确定且易于推理的特定习语,例如我上面演示的示例。 (就像 goto 通常是一个坏主意,但在 C 代码中本地使用时仍然有用,例如,用于某些样式的错误处理。)
    【解决方案3】:

    我经常读到异常有点慢,如果性能是一个问题(例如,在 Java、F# 等中),应该避免。这是否适用于常见的 OCaml 函数,例如 Hashtbl.find,它返回未找到元素的异常?

    没有。在主流语言中,关于异常缓慢和异常情况的民间传说是无稽之谈。异常只是另一个控制流结构。 OCaml 的异常快速且通常用于非异常情况。最后我发现(几年前)OCaml 中的异常比 C++ 快 6 倍左右,比 Java 快 100 倍左右,比 .NET 快 600 倍左右。

    人们有时避免 OCaml 中的异常不是出于与性能相关的原因,而是因为他们想要显式的本地控制流,通常强制调用者通过成功/失败联合类型使用 match,而不是潜在的非本地异常传播。他们这样做是为了提高正确性,这比性能更重要。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-05
      • 1970-01-01
      • 2021-12-14
      • 1970-01-01
      相关资源
      最近更新 更多