当前位置: 首页 > 知识库问答 >
问题:

为什么这种递归比等效迭代快得多?

张森
2023-03-14

我听说过很多次递归由于函数调用而很慢,但在这段代码中,它似乎比迭代解快得多。充其量,我通常期望编译器将递归优化为迭代(从程序集的角度来看,这似乎确实发生了)。

#include <iostream>

bool isDivisable(int x, int y)
{
    for (int i = y; i != 1; --i)
        if (x % i != 0)
            return false;
    return true;
}
bool isDivisableRec(int x, int y)
{
    if (y == 1)
        return true;
    return x % y == 0 && isDivisableRec(x, y-1);
}

int findSmallest()
{
    int x = 20;
    for (; !isDivisable(x,20); ++x);
    return x;
}

int main()
{
    std::cout << findSmallest() << std::endl;
}

在这里组装:https://gist.github.com/PatrickAupperle/2b56e16e9e5a6a9b251e

我很想知道这里发生了什么。我相信这是一个复杂的编译器优化,我可以惊讶地了解。

编辑:我刚刚意识到我忘了提到,如果我使用递归版本,它运行在大约。25秒,迭代,大约。6.

编辑2:我正在使用-O3编译

$ g++ --version
g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

不过,我真的不确定这有什么关系。

编辑3:Better benchmarking:
来源:http://gist.github.com/PatrickAupperle/ee8241ac51417437d012
输出:http://gist.github.com/PatrickAupperle/5870136a5552b83fd0f1
运行100次迭代显示出非常相似的结果

编辑4:
在Roman的建议下,我在编译标志中添加了-fno-内联-函数-fno-内联-小函数。效果对我来说极其奇怪。代码运行速度快了大约15倍,但是递归版本和迭代版本之间的比率保持相似。https://gist.github.com/PatrickAupperle/3a87eb53a9f11c1f0bec

共有1个答案

微生运浩
2023-03-14

使用这段代码,我还看到Cygwin中GCC 4.9.3的时间差异很大(有利于递归版本)。我明白了

13.411 seconds for iterative
4.29101 seconds for recursive

看看它用O3生成的汇编代码,我看到了两件事

>

  • 编译器将isDivisableRec中的尾递归替换为循环,然后展开循环:机器代码中循环的每次迭代都覆盖原始递归的两个级别。

    _Z14isDivisableRecii:
    .LFB1467:
        .seh_endprologue
        movl    %edx, %r8d
    .L15:
        cmpl    $1, %r8d
        je  .L18
        movl    %ecx, %eax          ; First unrolled divisibility check
        cltd
        idivl   %r8d
        testl   %edx, %edx
        je  .L20
    .L19:
        xorl    %eax, %eax
        ret
        .p2align 4,,10
    .L20:
        leal    -1(%r8), %r9d
        cmpl    $1, %r9d
        jne .L21
        .p2align 4,,10
    .L18:
        movl    $1, %eax
        ret
        .p2align 4,,10
    .L21:
        movl    %ecx, %eax          ; Second unrolled divisibility check
        cltd
        idivl   %r9d
        testl   %edx, %edx
        jne .L19
        subl    $2, %r8d
        jmp .L15
        .seh_endproc
    

    编译器通过将其提升到findsmalestrec来内联isDivisableRec的几个迭代。由于isDivisableRec的参数y的值被硬编码为20,编译器设法替换了20的迭代,19<代码>15,一些“神奇”的代码直接内联到FindSalestrec。对isDivisableRec的实际调用仅针对14的参数值y(如果发生)。

    这是findsmalestrec中的内联代码

        movl    $20, %ebx
        movl    $1717986919, %esi      ; Magic constants
        movl    $1808407283, %edi      ; for divisibility tests
        movl    $954437177, %ebp       ;
        movl    $2021161081, %r12d     ;
        movl    $-2004318071, %r13d    ;
        jmp .L28
        .p2align 4,,10
    .L29:                              ; The main cycle
        addl    $1, %ebx
    .L28:
        movl    %ebx, %eax             ; Divisibility by 20 test
        movl    %ebx, %ecx
        imull   %esi
        sarl    $31, %ecx
        sarl    $3, %edx
        subl    %ecx, %edx
        leal    (%rdx,%rdx,4), %eax
        sall    $2, %eax
        cmpl    %eax, %ebx
        jne .L29
        movl    %ebx, %eax             ; Divisibility by 19 test
        imull   %edi
        sarl    $3, %edx
        subl    %ecx, %edx
        leal    (%rdx,%rdx,8), %eax
        leal    (%rdx,%rax,2), %eax
        cmpl    %eax, %ebx
        jne .L29
        movl    %ebx, %eax             ; Divisibility by 18 test
        imull   %ebp
        sarl    $2, %edx
        subl    %ecx, %edx
        leal    (%rdx,%rdx,8), %eax
        addl    %eax, %eax
        cmpl    %eax, %ebx
        jne .L29
        movl    %ebx, %eax             ; Divisibility by 17 test
        imull   %r12d
        sarl    $3, %edx
        subl    %ecx, %edx
        movl    %edx, %eax
        sall    $4, %eax
        addl    %eax, %edx
        cmpl    %edx, %ebx
        jne .L29
        testb   $15, %bl               ; Divisibility by 16 test
        jne .L29
        movl    %ebx, %eax             ; Divisibility by 15 test
        imull   %r13d
        leal    (%rdx,%rbx), %eax
        sarl    $3, %eax
        subl    %ecx, %eax
        movl    %eax, %edx
        sall    $4, %edx
        subl    %eax, %edx
        cmpl    %edx, %ebx
        jne .L29
        movl    $14, %edx
        movl    %ebx, %ecx
        call    _Z14isDivisableRecii   ; call isDivisableRecii(x, 14)
        ...
    

    上述机器指令块位于每个jne之前。L29跳转是20、19的可分性测试<代码>15直接提升到FindSalestrec中。显然,对于运行时值y,它们比isDivisableRec中使用的测试更有效。如您所见,16整除性测试简单地实现为testb$15,%bl。因此,上述高度优化的代码很早就发现了x不可被y的高值整除。

    isDivisablefindSmallest都没有发生这种情况-它们基本上是按字面意思翻译的。甚至循环也没有展开。

    我相信第二个优化最大限度地发挥了作用。编译器使用高度优化的方法来检查更高y值的可整除性,这些值恰好在编译时已知。

    如果将isDivisableRec的第二个参数替换为“不可预测的”运行时值(而不是硬编码编译时常数),则应禁用此优化并使计时一致。我只是试了一下,结果是

    12.9 seconds for iterative
    13.26 seconds for recursive
    

  •  类似资料:
    • 对于一个特定的问题,我有两种实现,一种是递归的,另一种是迭代的,我想知道是什么导致迭代解决方案比递归解决方案慢约30%。 给定递归解决方案,我编写了一个迭代解决方案,使堆栈显式化。 显然,我只是模仿递归的作用,所以当然Python引擎可以更好地优化以处理簿记。但是我们可以编写具有类似性能的迭代方法吗? 我的案例研究是Euler项目的问题#14。 找到起始值低于一百万的最长Collatz链。 下面是

    • 在很多地方,我都看到过使用堆栈实现快速排序比使用递归更快的说法。这是真的吗?我知道编译器通常擅长将递归转换为迭代,但页面上的评论称,递归太复杂,无法优化。 快速排序还有哪些其他优化? 以下是我提到的一些地方,即递归实现优于递归实现:http://www.geeksforgeeks.org/iterative-quick-sort/ 尽管有上述优化,该函数仍然是递归的,并使用函数调用堆栈来存储l和h

    • 下面是一个最小的代码,用于重新创建让我怀疑的条件: 为什么在大小写中传递常量作为参数而在大小写中工作会出错? 错误的详细信息: 错误:将“const std::basic_string”作为“std::basic_string”的“this”参数传递

    • 如果说在任何地方都使用递归,那么可以使用for循环,对吗?如果递归通常比较慢,那么将其用于循环迭代的技术原因是什么? 如果总是可以将递归转换为for循环,那么有经验法则吗?

    • 问题内容: 因此,我在闲逛时使用了递归,我发现使用递归的循环比常规的while循环要慢得多,我想知道是否有人知道为什么。我已经包括了我下面所做的测试: 但是,在上一次测试中,我注意到如果删除该语句,则表明速度略有提高,因此我想知道if语句是否是造成循环速度差异的原因? 问题答案: 您已将函数编写为尾递归。在许多命令式和函数式语言中,这将触发尾部递归消除,在这种情况下,编译器用简单的JUMP替换了C

    • 我有两个gcd函数的实现: 函数gcd1是尾递归的,而gcd2使用的是时循环。 我已经验证了rubinius通过对阶乘函数进行基准测试来实现TCO,只有通过阶乘函数,基准测试才表明递归版本和迭代版本是“相同的ish”(我使用了基准测试IP)。 但对于上述情况,基准测试表明,gcd1比gcd2快至少两倍(递归比迭代快两倍,甚至更快)。 我用来基准测试的代码是这样的: 结果: 我正在运行Arch li