为什么这个递归比等价迭代快这么多
Why is this recursion so much faster than equivalent iteration?
我多次被告知,由于函数调用,递归很慢,但在这段代码中,它似乎比迭代解决方案快得多。在最好的情况下,我通常期望编译器将递归优化为迭代(查看程序集,似乎确实发生了)。
#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
我很想知道这里发生了什么。我确信这是一些棘手的编译器优化,我可以惊讶地了解。
编辑:我刚意识到我忘了说,如果我使用递归版本,它运行大约0.25秒,迭代,大约0.6。
编辑2:我正在使用
编译-O3$ g++ --version
g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
虽然,我不确定这有什么关系。
编辑3:
更好的基准:
来源:http://gist.github.com/PatrickAupperle/ee8241ac51417437d012
输出:http://gist.github.com/PatrickAupperle/5870136a5552b83fd0f1
运行100次迭代显示非常相似的结果
编辑4:
根据Roman的建议,我在编译标志中添加了-fno-inline-functions -fno-inline-small-functions。这种效果对我来说非常奇怪。代码运行速度大约快了15倍,但递归版本和迭代版本之间的比率仍然相似。https://gist.github.com/PatrickAupperle/3a87eb53a9f11c1f0bec
使用这段代码,我还看到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
编译器通过将提升到
以下是findSmallestRec
来内联几个isDivisableRec
的迭代。由于isDivisableRec
的y
参数的值被硬编码为20
,编译器成功地替换了20
,19
,…15
和一些"神奇的"代码直接内联到findSmallestRec
。对isDivisableRec
的实际调用只发生在y
参数值为14
的情况下(如果发生的话)。findSmallestRec
中的内联代码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
直接升入findSmallestRec
。显然,对于运行时值y
,它们比isDivisableRec
内部使用的测试更有效。如您所见,可被16整除测试被简单地实现为testb $15, %bl
。正因为如此,x
被y
的高值所不能整除的情况被上述高度优化的代码提前捕获。
这些都不会发生在isDivisable
和findSmallest
中——它们基本上是按字面翻译的。连循环都没有展开。
我相信这是第二个优化,造成了最大的不同。编译器使用高度优化的方法来检查较高的y
值的可除性,这些值恰好在编译时已知。
如果您将isDivisableRec
的第二个参数替换为"不可预测的"运行时值20
(而不是硬编码的编译时常数20
),它应该禁用此优化并使计时一致。我刚刚尝试了这个,最后以
12.9 seconds for iterative
13.26 seconds for recursive
- TSP递归解的迭代形式
- 此递归函数的每次迭代的值存储在哪里?
- 在迭代模板类型列表时无法停止递归
- 如何使用迭代器在 c++ 中的递归函数中传递值?
- 将多个非原始递归调用转换为迭代解决方案
- 迭代 /递归
- 通过提升属性树(递归方式)发出 YAML 迭代
- 树不维护递归迭代器成员
- 使用迭代器对向量的C 递归初始化产生不一致的结果
- 如何将递归函数(具有两个基本情况)转换为迭代函数
- 将递归函数转换为迭代函数
- 从递归算法到迭代算法
- Union-Find方法性能,迭代与递归
- 递归到迭代而不重新绑定引用
- C++ 递归可迭代访问
- 递归可迭代模板函数 C++
- 为什么后缀失败并且前缀在传递迭代器作为参数并在尾部位置递归时工作正常
- C 递归代码迭代
- 从预序遍历迭代(不是递归)构造二叉搜索树
- 以迭代方式编写递归代码