Java - 为什么尾调用递归模式比正常的递归模式更快?

时间:2017-12-21 21:06:09

标签: java performance recursion tail-recursion

我想知道为什么"尾部调用"模式在Java中表现得更好,然后使用经典的递归模式。

给出以下示例:

public static long facultyApproachPureRec(long n)
{
    if(n == 0 || n == 1)
    {
        return 1;
    }

    long temp = facultyApproachPureRec(n-1);
    return (n*temp);
}

public static long facultyApproachTailEnd(long n, long m)
{
    if(n == 0 || n == 1)
    {
        return m;
    }

    long temp = n*m;
    return facultyApproachTailEnd(n-1, temp);
}

facultyApproachTailEnd总是比纯递归解决方案更快。这让我想知道为什么因为Java不支持尾调用优化(我证明了这一点:我可以轻松获得StachOverflowError)。 因为这个事实我会假设尾调用解决方案比纯递归求解更差,因为我们总是需要传递双倍量的变量。

有人可以解释一下为什么基准测试表明尾部调用总是比其他人快吗?

注意:在c#中,它的行为就像我预期的那样:尾调用解决方案总是比纯递归解决方案慢。

基准代码:

    public static void main(String[] args)
{
    for(int k = 0; k < 10; k++) {
        long begin = System.nanoTime();
        for (int i = 0; i < 20000; i++) {
            if (facultyApproachPureRec(i) == 0)
            {
                i++;
            }
        }

        long end = System.nanoTime();
        long elapsed = TimeUnit.MILLISECONDS.convert((end - begin), TimeUnit.NANOSECONDS);
        System.out.println("Pure recursion took " + elapsed);

        begin = System.nanoTime();
        for (int i = 0; i < 20000; i++) {
            if(facultyApproachTailEnd(i, 1) == 0)
            {
                i++;
            }
        }

        end = System.nanoTime();
        elapsed = TimeUnit.MILLISECONDS.convert((end - begin), TimeUnit.NANOSECONDS);
        System.out.println("Tail end took " + elapsed);
    }
}

一些结果(在3台机器上验证,都或多或少相同):

Pure recursion took 623
Tail end took 461
Pure recursion took 570
Tail end took 418
Pure recursion took 913
Tail end took 581
Pure recursion took 580
Tail end took 417
Pure recursion took 607
Tail end took 411
Pure recursion took 601
Tail end took 417
Pure recursion took 769
Tail end took 372
Pure recursion took 527
Tail end took 568
Pure recursion took 542
Tail end took 419
Pure recursion took 632
Tail end took 333

感谢所有答案。

编辑:我在写这个问题之前经常测试这个(1000次迭代,直接通过cmd(所以没有IDE))。如果我的测试错了我很抱歉。我在大学以外没有使用过Java。

Edit2:我在下面添加了生成的ByteCode。我不熟悉Java字节代码(我更习惯于clr),但从简短的角度来看,它们看起来非常相同,只是尾部调用解决方案内部有更多代码并使用/维护更多变量。

    public static facultyApproachPureRec(J)J
   L0
    LINENUMBER 39 L0
    LLOAD 0
    LCONST_0
    LCMP
    IFEQ L1
    LLOAD 0
    LCONST_1
    LCMP
    IFNE L2
   L1
    LINENUMBER 41 L1
   FRAME SAME
    LCONST_1
    LRETURN
   L2
    LINENUMBER 44 L2
   FRAME SAME
    LLOAD 0
    LCONST_1
    LSUB
    INVOKESTATIC Logik/Rekursion/MainLogik.facultyApproachPureRec (J)J
    LSTORE 2
   L3
    LINENUMBER 45 L3
    LLOAD 0
    LLOAD 2
    LMUL
    LRETURN
   L4
    LOCALVARIABLE n J L0 L4 0
    LOCALVARIABLE temp J L3 L4 2
    MAXSTACK = 4
    MAXLOCALS = 4

  // access flags 0x9
  public static facultyApproachTailEnd(JJ)J
   L0
    LINENUMBER 50 L0
    LLOAD 0
    LCONST_0
    LCMP
    IFEQ L1
    LLOAD 0
    LCONST_1
    LCMP
    IFNE L2
   L1
    LINENUMBER 52 L1
   FRAME SAME
    LLOAD 2
    LRETURN
   L2
    LINENUMBER 55 L2
   FRAME SAME
    LLOAD 0
    LLOAD 2
    LMUL
    LSTORE 4
   L3
    LINENUMBER 56 L3
    LLOAD 0
    LCONST_1
    LSUB
    LLOAD 4
    INVOKESTATIC Logik/Rekursion/MainLogik.facultyApproachTailEnd (JJ)J
    LRETURN
   L4
    LOCALVARIABLE n J L0 L4 0
    LOCALVARIABLE m J L0 L4 2
    LOCALVARIABLE temp J L3 L4 4
    MAXSTACK = 4
    MAXLOCALS = 6
}

0 个答案:

没有答案