【问题标题】:Strange recursion optimization by javajava奇怪的递归优化
【发布时间】:2012-06-05 23:05:12
【问题描述】:

当我试图回答这个问题时,我遇到了一些奇怪的结果:How to improve the performance of the recursive method?

但是您不需要阅读该帖子。我将在这里给出相关的上下文。 这可能看起来很长,但如果你通读一遍,实际上并没有那么复杂。我希望这对所有人来说都很有趣。对于上下文,

syra(n) = { 1 if n=1; 
            n + syra(n/2) if n is even; and 
            n + syra(3n+1) if n is odd
          }

syralen(n) = No. of steps to calculate syra (n)

例如, syralen(1)=1, syralen(2)=2 since we need to go two steps. syra(10) = 10 + syra(5) = 15 + syra(16) = 31 + syra(8) = 39 + syra(4) = 43 + syra(2) = 45 + syra(1) = 46。所以 syra(10) 需要 7 个步骤。因此syralen(10)=7

最后,

lengths(n) = syralen(1)+syralen(2)+...+syralen(n)

那里的问题海报试图计算lengths(n)

我的问题是关于 Op 发布的递归解决方案(这是该问题中的第二个 sn-p)。我会在这里转发:

public class SyraLengths{

        int total=1;
        public int syraLength(long n) {
            if (n < 1)
                throw new IllegalArgumentException();
            if (n == 1) {
                int temp=total;
                total=1;
                return temp;
            }
            else if (n % 2 == 0) {
                total++;
                return syraLength(n / 2);
            }
            else {
                total++;
                return syraLength(n * 3 + 1);
            }
        }

        public int lengths(int n){
            if(n<1){
                throw new IllegalArgumentException();
            }
            int total=0;
            for(int i=1;i<=n;i++){
                total+=syraLength(i);
            }

            return total;
        }

        public static void main(String[] args){
            System.out.println(new SyraLengths().lengths(5000000));
        }
       }

这肯定是一种不寻常(可能不推荐)的递归方式,但它确实计算出正确的东西,我已经证实了这一点。我试着写一个更常用的递归版本:

public class SyraSlow {

    public long lengths(int n) {
        long total = 0;
        for (int i = 1; i <= n; ++i) {
            total += syraLen(i);
        }
        return total;
    }

    private long syraLen(int i) {
        if (i == 1)
            return 1;
        return 1 + ((i % 2 == 0) ? syraLen(i / 2) : syraLen(i * 3 + 1));
    }

现在这是奇怪的部分 - 我尝试测试上述两个版本的性能,例如:

public static void main(String[] args){
            long t1=0,t2=0;
            int TEST_VAL=50000;

            t1 = System.currentTimeMillis();
            System.out.println(new SyraLengths().lengths(TEST_VAL));
            t2 = System.currentTimeMillis();
            System.out.println("SyraLengths time taken: " + (t2-t1));

            t1 = System.currentTimeMillis();
            System.out.println(new SyraSlow().lengths(TEST_VAL));
            t2 = System.currentTimeMillis();
            System.out.println("SyraSlow time taken: " + (t2-t1));
        }

对于TEST_VAL=50000,输出为:

5075114
SyraLengths time taken: 44
5075114
SyraSlow time taken: 31

正如预期的那样(我猜)普通递归稍微好一些。但是当我更进一步并使用TEST_VAL=500000 时,输出是:

62634795
SyraLengths time taken: 378
Exception in thread "main" java.lang.StackOverflowError
    at SyraSlow.syraLen(SyraSlow.java:15)
    at SyraSlow.syraLen(SyraSlow.java:15)
    at SyraSlow.syraLen(SyraSlow.java:15)

为什么会这样? Java 在这里做了什么样的优化,SyraLengths 版本没有命中 StackOverflow(它甚至在 TEST_VAL=5000000 上也有效)? 我什至尝试使用基于累加器的递归版本,以防我的 JVM 进行一些尾调用优化:

private long syraLenAcc(int i, long acc) {
        if (i == 1) return acc;
    if(i%2==0) {
        return syraLenAcc(i/2,acc+1);
    }
    return syraLenAcc(i * 3 + 1, acc+1);
    }

但我仍然得到相同的结果(因此这里没有尾调用优化)。那么,这里发生了什么?

P.S:如果您能想到任何标题,请修改为更好的标题。

【问题讨论】:

    标签: java recursion stack-overflow tail-recursion


    【解决方案1】:

    在原始版本中,尾递归优化是可能的(在 JIT 内)。不过,不知道它是否真的发生过。但有可能原始版本在堆 [呃,我的意思是堆栈] 使用方面效率更高。 (或者可能存在粗略检查不明显的功能差异。)

    【讨论】:

    • 绝对堆栈使用。如果函数的最后一条语句是对其自身的调用,尾递归可防止大量堆栈使用。在这种情况下,粗略地说,可以用跳转代替调用。该函数基本上被包装成一种循环,将递归转换为迭代。 (不要对我大喊大叫,我知道这有点过于简单化了。)
    • 但是我已经尝试了带有累加器的第二个版本,如果发生尾调用优化,它应该也有帮助
    • 据此:openjdk.java.net/projects/mlvm/subprojects.html 和此:bugs.sun.com/bugdatabase/view_bug.do?bug_id=6804517 还没有尾部优化。看看Tail calls and tail recursion。也看这里:stackoverflow.com/a/105897/639753
    • ? 语句可能会混淆优化器并导致它放弃尾递归优化。
    • @Xeon..exactly.. 这就是为什么我无法理解这一点。
    【解决方案2】:

    嗯,原来有一个简单的解释:

    我使用long syraLen(int n) 作为方法签名。但n 的值实际上可以比Integer.MAX_VALUE 大得多。所以syraLen 得到了负面的输入,这就是问题所在。如果我将其更改为long syraLen(long n),一切都会完美运行!我希望我也把if(n &lt; 1) throw new IllegalArgumentException(); 像原来的海报一样。会为我节省一些时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-08-04
      • 2010-10-02
      • 1970-01-01
      • 1970-01-01
      • 2012-08-11
      • 2021-03-28
      • 1970-01-01
      相关资源
      最近更新 更多