我多次被告知由于函数调用递归速度很慢,但在这段代码中,它似乎比迭代解决方案快得多。最好的情况是,我通常希望编译器优化递归到迭代(看着程序集,似乎确实发生了)。
#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秒,迭代,大约.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
答案 0 :(得分:6)
使用this代码我也看到了Cygwin中GCC 4.9.3的大的时序差异(支持递归版本)。我得到了
13.411 seconds for iterative
4.29101 seconds for recursive
查看它使用-O3
生成的汇编代码,我看到了两件事
编译器用isDivisableRec
替换了一个循环的尾递归,然后展开循环:机器代码中循环的每次迭代都包含 2 原始递归的级别。
_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
编译器内联 isDivisableRec
的几次迭代,将它们提升为findSmallestRec
。由于y
的{{1}}参数的值被硬编码为isDivisableRec
,因此编译器设法替换20
,20
的迭代... {{1}与一些&#34;魔法&#34;代码直接内联到19
。对[{1}}的实际调用仅适用于15
的{{1}}参数值(如果它发生的话)。
这是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
。如您所见,16测试的可分性仅作为15
实现。因此,上述高度优化的代码可以及早发现findSmallestRec
对isDivisableRec
的高值不可分割性。
y
和testb $15, %bl
都不会发生这种情况 - 它们基本上是按字面翻译的。甚至周期也没有展开。
我相信这是第二次优化,可以带来最大的不同。编译器使用高度优化的方法来检查更高x
值的可分性,这在编译时恰好是已知的。
如果将y
的第二个参数替换为&#34;不可预测的&#34;运行时值isDivisable
(而不是硬编码的编译时常量findSmallest
),它应该禁用此优化并使时间排成一行。我只是尝试了这个并最终得到了
y