为什么OCaml中的递归比C ++或Java更有效?

时间:2014-03-03 04:33:36

标签: recursion ocaml

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

  

与C ++和Java相反,O'Caml [sic]中的递归与迭代一样有效

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

OCaml真的有一种机制使递归比C ++和Java更有效吗?

2 个答案:

答案 0 :(得分:5)

是的,在某些情况下。它被称为尾调用优化。例如,采用以下C-ish因子函数:

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

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

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

实际上,上面的代码也将深入到调用堆栈中,就像上面的C-ish代码一样。这是C-ish函数,它实现了相同但循环:

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

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

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

acc参数可以被认为是“累积”所有先前对factorial的递归调用的结果。关键的效果是上面的表达式“n * factorial(n-1)”变形为下面的表达式“factorial(acc * n)(n-1)”。在第二个表达式中,对factorial的递归调用是表达式的顶级,这意味着不需要执行额外的操作来获取函数的返回值。第一个表达式不是这样,其中顶级操作是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

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

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

答案 1 :(得分:0)

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

请参阅: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. 所有其他递归类型都使用迭代和堆栈对象进行优化,以模拟递归调用(我没有找到任何支持此文档的文档)。
  3. 我知道至少.Net / C#不太可能跨功能边界进行优化。例如,必须使用属性显式请求函数的内嵌,甚至可能不会。 JIT可以内联并可能优化递归但不会出现。这可能适用于Java。

    我不确定C ++,并且取决于你使用的编译器。