为什么当递归函数结果相乘时,g++ 仍然优化尾递归
Why g++ still optimizes tail recursion when the recursion function result is multiplied?
他们说,尾递归优化仅在调用刚好从函数返回之前时才有效。因此,他们将以下代码作为 C 编译器不应优化的示例:
long long f(long long n) {
return n > 0 ? f(n - 1) * n : 1;
}
因为那里的递归函数调用乘以 n
这意味着最后一个操作是乘法,而不是递归调用。但是,它甚至在-O1
层面上:
recursion`f:
0x100000930 <+0>: pushq %rbp
0x100000931 <+1>: movq %rsp, %rbp
0x100000934 <+4>: movl $0x1, %eax
0x100000939 <+9>: testq %rdi, %rdi
0x10000093c <+12>: jle 0x10000094e
0x10000093e <+14>: nop
0x100000940 <+16>: imulq %rdi, %rax
0x100000944 <+20>: cmpq $0x1, %rdi
0x100000948 <+24>: leaq -0x1(%rdi), %rdi
0x10000094c <+28>: jg 0x100000940
0x10000094e <+30>: popq %rbp
0x10000094f <+31>: retq
他们说:
因此,您的最终规则足够正确。但是,返回
n * fact(n - 1)
确实在尾部位置有操作!这是乘法*
,这将是函数做的最后一件事 在它返回之前。在某些语言中,这实际上可能是 实现为函数调用,然后可以是尾调用 优化。
但是,正如我们从 ASM 列表中看到的那样,乘法仍然是一个 ASM 指令,而不是一个单独的函数。所以我真的很难看到累加器方法的区别:
int fac_times (int n, int acc) {
return (n == 0) ? acc : fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times(n, 1);
}
这会产生
recursion`fac_times:
0x1000008e0 <+0>: pushq %rbp
0x1000008e1 <+1>: movq %rsp, %rbp
0x1000008e4 <+4>: testl %edi, %edi
0x1000008e6 <+6>: je 0x1000008f7
0x1000008e8 <+8>: nopl (%rax,%rax)
0x1000008f0 <+16>: imull %edi, %esi
0x1000008f3 <+19>: decl %edi
0x1000008f5 <+21>: jne 0x1000008f0
0x1000008f7 <+23>: movl %esi, %eax
0x1000008f9 <+25>: popq %rbp
0x1000008fa <+26>: retq
我错过了什么吗?或者只是编译器变得更聪明了?
正如您在汇编代码中看到的那样,编译器足够聪明,可以将您的代码转换为基本上等效于(忽略不同数据类型)的循环:
int fac(int n)
{
int result = n;
while (--n)
result *= n;
return result;
}
GCC 足够聪明,知道每次调用原始f
所需的状态可以在整个递归调用序列中保存在两个变量(n
和 result
)中,因此不需要堆栈。可以这么说,它可以将f
转换为fac_times
,也可以将两者转换为fac
。这很可能不仅是严格意义上的尾部调用优化的结果,而且是 GCC 用于优化的其他启发式方法的负载之一。
(关于这里使用的特定启发式方法,我无法更详细地介绍它们,因为我对它们了解不够。
非累加器f
不是尾递归的。 编译器的选项包括通过转换它或call
/某些insns/ret
将其转换为循环,但它们不包括没有其他转换的jmp f
。
尾调用优化适用于以下情况:
int ext(int a);
int foo(int x) { return ext(x); }
来自 Godbolt 的 ASM 输出:
foo: # @foo
jmp ext # TAILCALL
尾调用优化意味着将函数(或递归)保留为jmp
而不是ret
。 其他任何事情都不是尾调用优化。 不过,使用jmp
优化的尾递归实际上是一个循环。
一个好的编译器会做进一步的转换,尽可能将条件分支放在循环的底部,删除无条件分支。 (在asm中,循环的do{}while()
风格是最自然的)。
- 空基优化子对象的地址
- 关闭||运算符优化
- 如何解决gcc编译器优化导致的centos双编译器设置中的分段错误
- 返回值优化:显式移动还是隐式
- 人脸跟踪arduino代码的优化
- 使用仅使用一次的变量调用的复制构造函数.这可能是通过调用move构造函数进行编译器优化的情况吗
- 纯函数,为什么没有优化
- 为什么大多数 pair 实现默认不使用压缩(空基优化)?
- 如何以优化的方式同时迭代两个间距不相等的数组
- 小字符串优化(调试与发布模式)
- 浮点定向舍入和优化
- Visual Studio 调试优化如何工作?
- 为什么开关的优化方式与 c/c++ 中的链接不同?
- 线性优化目标函数中的绝对值
- GCC 会优化内联访问器吗?
- gcc 如何优化此循环?
- 如何防止 CUDA-GDB 中的<优化输出>值
- 为什么我的程序在 O0 和 O2 的优化级别返回不同的结果
- 这个C++编译器优化(在自身的实例上调用对象自己的构造函数)的名称是什么,它是如何工作的?
- 使用 std::p air 进行返回值优化