java中斐波那契函数的尾调用优化

发布于 2024-10-27 01:31:07 字数 2332 浏览 7 评论 0原文

我正在研究尾调用递归,并遇到了一些提到的文档。 Sun Java 没有实现尾部调用优化。 我编写了以下代码以三种不同的方式计算斐波那契数: 1. 迭代 2. 头递归 3.尾递归

public class Fibonacci {
    public static void main(String[] args) throws InterruptedException {
        int n = Integer.parseInt(args[0]);
        System.out.println("\n Value of n : " + n);
        System.out.println("\n Using Iteration : ");
        long l1 = System.nanoTime();
        fibonacciIterative(n);
        long l2 = System.nanoTime();
        System.out.println("iterative time = " + (l2 - l1));
        System.out.println(fibonacciIterative(n));

        System.out.println("\n Using Tail recursion : ");
        long l3 = System.nanoTime();
        fibonacciTail(n);
        long l4 = System.nanoTime();
        System.out.println("Tail recursive time = " + (l4 - l3));
        System.out.println(fibonacciTail(n));

        System.out.println("\n Using Recursion : ");
        long l5 = System.nanoTime();
        fibonacciRecursive(n);
        long l6 = System.nanoTime();
        System.out.println("Head recursive time = " + (l6 - l5));
    }

    private static long fibonacciRecursive(int num) {
        if (num == 0) {
            return 0L;
        }
        if (num == 1) {
            return 1L;
        }
        return fibonacciRecursive(num - 1) + fibonacciRecursive(num - 2);
    }

    private static long fibonacciIterative(int n) throws InterruptedException {
        long[] arr = new long[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        for (int i = 2; i <= n; i++) {
            // Thread.sleep(1);
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }

    private static long fibonacciTail(int n) {
        if (n == 0)
            return 0;
        return fibHelper(n, 1, 0, 1);
    }

    private static long fibHelper(int n, int m, long fibM_minus_one, long fibM) {
        if (n == m)
            return fibM;
        return fibHelper(n, m + 1, fibM, fibM_minus_one + fibM);
    }
}

在运行这个程序时,我得出了一些结果:

  1. 头递归方法对于n>50没有完成。程序看起来像是挂起了。知道为什么会发生这种情况吗?
  2. 与头递归相比,尾递归方法花费的时间明显更少。 有时比迭代方法花费的时间更少。这是否意味着java内部做了一些Tail调用优化? 如果确实如此,为什么我这样做会在 n > 时给出 StackOverflowError 5000?

系统规格:

Intel core 5 处理器、

Windows XP、

32 位 Java 1.6

JVM 的默认堆栈大小。

I was studying about Tail call recursion and came across some documentation that mentioned. Sun Java doesn't implement tail call optimization.
I wrote following code to calculate fibonacci number in 3 different ways:
1. Iterative
2. Head Recursive
3. Tail Recursive

public class Fibonacci {
    public static void main(String[] args) throws InterruptedException {
        int n = Integer.parseInt(args[0]);
        System.out.println("\n Value of n : " + n);
        System.out.println("\n Using Iteration : ");
        long l1 = System.nanoTime();
        fibonacciIterative(n);
        long l2 = System.nanoTime();
        System.out.println("iterative time = " + (l2 - l1));
        System.out.println(fibonacciIterative(n));

        System.out.println("\n Using Tail recursion : ");
        long l3 = System.nanoTime();
        fibonacciTail(n);
        long l4 = System.nanoTime();
        System.out.println("Tail recursive time = " + (l4 - l3));
        System.out.println(fibonacciTail(n));

        System.out.println("\n Using Recursion : ");
        long l5 = System.nanoTime();
        fibonacciRecursive(n);
        long l6 = System.nanoTime();
        System.out.println("Head recursive time = " + (l6 - l5));
    }

    private static long fibonacciRecursive(int num) {
        if (num == 0) {
            return 0L;
        }
        if (num == 1) {
            return 1L;
        }
        return fibonacciRecursive(num - 1) + fibonacciRecursive(num - 2);
    }

    private static long fibonacciIterative(int n) throws InterruptedException {
        long[] arr = new long[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        for (int i = 2; i <= n; i++) {
            // Thread.sleep(1);
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }

    private static long fibonacciTail(int n) {
        if (n == 0)
            return 0;
        return fibHelper(n, 1, 0, 1);
    }

    private static long fibHelper(int n, int m, long fibM_minus_one, long fibM) {
        if (n == m)
            return fibM;
        return fibHelper(n, m + 1, fibM, fibM_minus_one + fibM);
    }
}

On running this program I drew some results:

  1. Head Recursive method does not finish for n>50. Program looked like hanged. Any idea, why this could happen?
  2. Tail recursive method took significantly less time as compared to Head recursion. Sometimes took even less time than Iterative method. Does it mean that java does some Tail call optimization internally?
    And if it does, why I did it give StackOverflowError at n > 5000?

System specs:

Intel core 5 processor,

Windows XP,

32 bit Java 1.6

Default stack size for JVM.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

带上头具痛哭 2024-11-03 01:31:07

这是否意味着java内部做了一些Tail调用优化?

不,事实并非如此。 HotSpot JIT 编译器不实现尾部调用优化。

您观察到的结果是您在未考虑 JVM 预热的 Java 基准测试中看到的典型异常情况。例如,调用一个方法的“前几次”,它将由解释器执行。然后 JIT 编译器将编译该方法......并且它会变得更快。

为了获得有意义的结果,请围绕整个批次进行循环并运行多次,直到时间稳定为止。然后丢弃早期迭代的结果。

...为什么我这样做会在 n > 处给出 StackOverflowError 5000?

这只是表明没有发生任何尾调用优化的证据。

Does it mean that java does some Tail call optimization internally?

No, it does not. The HotSpot JIT compilers do not implement tail-call optimization.

The results you are observing are typical of the anomalies that you see in a Java benchmark that doesn't take account of JVM warmup. For instance, the "first few" times a method is called, it will be executed by the interpreter. Then the JIT compiler will compile the method ... and it will get faster.

To get meaningful results, put a loop around the whole lot and run it a number of times until the timings stabilize. Then discard the results from the early iterations.

... why I did it give StackOverflowError at n > 5000?

That's just evidence that there isn't any tail-call optimization happening.

毁梦 2024-11-03 01:31:07

对于第一个问题,2^50(或接近的值)是多少?递归 Fib 函数中的每个数字 N 都会调用它两次(之前的 2 次)。其中每一个都调用 2 个先前的迭代,等等......因此它会增长到 2^(Nk) 的递归(k 可能是 2 或 3)。

第二个问题是因为第二个问题是直N递归。它不是双头(N-1),(N-2),而是简单地从 M=1、M=2...M=N 构建。每一步都会保留 N-1 值用于相加。由于它是 O(N) 操作,因此它与迭代方法相当,唯一的区别在于 JIT 编译器如何优化它。不过,递归的问题在于,堆栈到帧上的每个级别都需要巨大的内存占用 - 在某些限制下,您会耗尽内存或堆栈空间。 它通常仍然比迭代方法慢。

For the first question, what is 2^50 (or something close)? Each number N in a recursive Fib function calls it twice (prior 2). Each of those calls 2 prior iterations, etc.. so it's grows to 2^(N-k) of recursion (k is probably 2 or 3).

The 2nd question is because the 2nd one is a straight N recursion. Instead of going double headed (N-1),(N-2), it simply builds up from M=1, M=2... M=N. Each step of the way, the N-1 value is retained for adding. Since it is an O(N) operation, it is comparable to the iterative method, the only difference being how the JIT compiler optimizes it. The problem with recursion though is that it requires a huge memory footprint for each level that you stack onto the frame - you run out of memory or stack space at some limit. It should still generally be slower than the iterative method.

贵在坚持 2024-11-03 01:31:07

关于第 1 点:在没有记忆的情况下递归计算斐波那契数会导致运行时间以 n 为指数。这适用于任何不会自动记忆函数结果的编程语言(例如大多数主流非函数式语言,例如 Java、C#、C++ 等)。原因是相同的函数会被一遍又一遍地调用 - 例如 f(8) 将调用 f(7)f(6); f(7) 将调用 f(6)f(5),因此 f(6)被叫两次。这种影响会传播并导致函数调用数量呈指数增长。以下是调用哪些函数的可视化:

f(8)
 f(7)
  f(6)
   f(5)
    f(4)
     ...
    f(3)
     ...
   f(4)
    ...
  f(5)
   f(4)
    ...
   f(3)
    ...
 f(6)
  f(5)
   ...
  f(4)
   ...

Regarding point 1: Computing Fibonacci numbers recursively without memoization leads to a run time that is exponential in n. This goes for any programming language that does not automatically memoize function results (such as most mainstream non-functional languages, e.g. Java, C#, C++, ...). The reason is that the same functions will get called over and over again - e.g. f(8) will call f(7) and f(6); f(7) will call f(6) and f(5), so that f(6) gets called twice. This effect propagates and causes an exponential growth in the number of function calls. Here's a visualization of which functions get called:

f(8)
 f(7)
  f(6)
   f(5)
    f(4)
     ...
    f(3)
     ...
   f(4)
    ...
  f(5)
   f(4)
    ...
   f(3)
    ...
 f(6)
  f(5)
   ...
  f(4)
   ...
凉月流沐 2024-11-03 01:31:07

您可以使用 Memoization 来避免头递归。

我测试了以下代码,当 N <=40 时,这种方法很糟糕,因为 Map 需要权衡。

private static final Map<Integer,Long> map = new HashMap<Integer,Long>();

private static long fibonacciRecursiveMemoization(int num) {
    if (num == 0) {
        return 0L;
    }
    if (num == 1) {
        return 1L;
    }

    int num1 = num - 1;
    int num2 = num - 2;

    long numResult1 = 0;
    long numResult2 = 0;

    if(map.containsKey(num1)){
        numResult1 = map.get(num1);
    }else{
        numResult1 = fibonacciRecursiveMemoization(num1);
        map.put(num1, numResult1);
    }

    if(map.containsKey(num2)){
        numResult2 = map.get(num2);
    }else{
        numResult2 = fibonacciRecursiveMemoization(num2);
        map.put(num2, numResult2);
    }

    return numResult1 + numResult2;
}

当 n 的值:44

使用迭代:
迭代时间 = 6984

使用尾递归:
尾递归时间 = 8940

使用记忆递归:
记忆递归时间 = 1799949

使用递归:
头递归时间=12697568825

You can use Memoization to avoid head recursion.

I have tested the following code , when N <=40 , that approach is bad because Map has trade-off.

private static final Map<Integer,Long> map = new HashMap<Integer,Long>();

private static long fibonacciRecursiveMemoization(int num) {
    if (num == 0) {
        return 0L;
    }
    if (num == 1) {
        return 1L;
    }

    int num1 = num - 1;
    int num2 = num - 2;

    long numResult1 = 0;
    long numResult2 = 0;

    if(map.containsKey(num1)){
        numResult1 = map.get(num1);
    }else{
        numResult1 = fibonacciRecursiveMemoization(num1);
        map.put(num1, numResult1);
    }

    if(map.containsKey(num2)){
        numResult2 = map.get(num2);
    }else{
        numResult2 = fibonacciRecursiveMemoization(num2);
        map.put(num2, numResult2);
    }

    return numResult1 + numResult2;
}

when the value of n : 44

Using Iteration :
iterative time = 6984

Using Tail recursion :
Tail recursive time = 8940

Using Memoization Recursion :
Memoization recursive time = 1799949

Using Recursion :
Head recursive time = 12697568825

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文