【问题标题】:Why is recursion more efficient in OCaml than C++ or Java?为什么 OCaml 中的递归比 C++ 或 Java 更有效?
【发布时间】:2014-03-04 03:14:26
【问题描述】:

caml.inria.fr 网站上的这篇文章 A C++/Java programmer's introduction to Objective Caml 说...

与 C++ 和 Java 不同,O'Caml [原文如此] 中的递归与迭代一样高效

对于像阶乘这样的东西,带有可变变量的循环似乎比递归调用所涉及的堆栈操作更有效。

OCaml 真的有一种机制可以让递归比 C++ 和 Java 更高效吗?

【问题讨论】:

  • 一些“好的”递归调用在 OCaml 和其他函数式语言中被编译为单纯的跳转,因此它与循环等效。从技术上讲,“好”意味着在尾部位置,但在我看来,该文档故意跳过了这一点,因为它只是介绍性的目的。
  • 适当使用“[sic]”,但“OCaml”也不是你写的那样。 yquem.inria.fr/pipermail/caml-announce/2012-July/000000.html(“OCaml”已经是我在 2001 年左右写博士时应该写的方式,但当时还不是官方的)。

标签: recursion ocaml


【解决方案1】:

是的,在某些情况下。这称为尾调用优化。以下面的 C-ish 阶乘函数为例:

int factorial(int n) 
{
        if (n < = 1)
                return 1;
        else
                return n * factorial(n – 1);
}

此函数在“展开”备份之前深入调用堆栈以获得结果。以下是 OCaml 等价物:

let factorial n = if n <= 1 then 1 else n * factorial (n - 1) 

现在实际上,上面的代码也将深入调用堆栈 n 层,就像上面的 C-ish 代码一样。这是完成相同但带有循环的 C-ish 函数:

int factorial(int n)
{
        int ret = 1;
        for (; n > 1; n--)
                ret *= n;
        return ret;
} 

当然,这个函数可以被调用任意次数而不会溢出堆栈(即使你很快就会溢出一个 32 位的 int)。实际上可以在 OCaml 中编写同义函数。现在,OCaml 的版本将再次使用递归。但是,如果我们在第一个函数中添加一个“累加器”参数,它可以重写为:

let factorial acc n = if n <= 1 then acc else factorial (acc * n) (n – 1)

acc 参数可以被认为是“累积”所有先前对阶乘的递归调用的结果。关键效果是上面的表达式“n * factorial (n -1)”变成了下面的表达式“factorial (acc * n) (n – 1)”。在第二个表达式中,对阶乘的递归调用是表达式的顶层,这意味着不需要对其执行额外的操作来获取函数的返回值。这不适用于第一个表达式,其中顶级运算是 n – 1 的阶乘结果与 n 的乘积。当递归函数调用是顶级表达式时,它被认为是“尾调用”,编译器可以并将其优化为有效的循环。在第一个函数上调用“factorial 2000000”将(可能)导致堆栈溢出,但在第二个函数上调用“factorial 1 2000000”不会。此外,您可能会发现第二个 OCaml 函数在性能方面与 C 等效函数相当(它可能会慢一点,但不是数量级或其他任何东西)。

顺便说一句,您可能会问自己,“但尾递归函数有一个不必要的额外 'acc' 参数,在用户最初调用时应该始终为 1,这不是很麻烦吗?”是的,是的。这个问题很容易通过将尾递归函数嵌套到一个“包装器”函数中来解决,该函数使用正确的初始累积值调用它,如下所示:

let factorial n =
    let loop acc n' = if n' <= 1 then acc else loop (acc * n') (n' – 1) in
    loop 1 n

在这里,我将上面的尾递归阶乘函数重命名为“循环”并将其嵌套在一个函数中,然后使用正确的初始累加器 1 调用它。

通常情况下,这些尾递归模式可以用标准库中的高阶函数代替,例如 List.fold,但并非总是如此。

【讨论】:

    【解决方案2】:

    语言的函数式编程结构可能使编译器更容易识别和优化递归。

    见:http://ocaml.org/learn/tutorials/if_statements_loops_and_recursion.html

    If you write your code recursively instead of iteratively then you necessarily run out of stack space on large inputs, right?

    In fact, wrong. Compilers can perform a simple optimisation on certain types of recursive functions to turn them into while loops

    我建议:

    1. OCaml 的编译器将尾递归优化为迭代结构;并且可能
    2. 所有其他递归类型都通过迭代和堆栈对象进行优化,以模拟递归调用(我没有找到任何支持这一点的文档)。

    我知道至少 .Net/C# 不太可能跨函数边界进行优化。例如,必须使用属性显式请求内联函数,甚至可能仍然不需要。 JIT 可能内联并可能优化递归,但似乎没有。这可能适用于 Java。

    我不确定 C++,这取决于您使用的编译器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-02-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-31
      • 2014-04-05
      相关资源
      最近更新 更多