C++ 为什么是g++;当递归函数结果相乘时,仍然优化尾部递归?

C++ 为什么是g++;当递归函数结果相乘时,仍然优化尾部递归?,c++,assembly,optimization,g++,tail-recursion,C++,Assembly,Optimization,G++,Tail Recursion,他们说,尾部递归优化只在调用刚好在函数返回之前起作用。因此,他们将此代码作为C编译器不应优化的示例: 长f(长n){ 返回n>0?f(n-1)*n:1; } 因为递归函数调用是与n相乘的,这意味着最后一个操作是乘法,而不是递归调用。但是,它甚至处于-O1级别: recursion`f: 0x10000930:pushq%rbp 0x10000931:movq%rsp,%rbp 0x10000934:movl$0x1,%eax 0x10000939:testq%rdi,%rdi 0x100000

他们说,尾部递归优化只在调用刚好在函数返回之前起作用。因此,他们将此代码作为C编译器不应优化的示例:

长f(长n){
返回n>0?f(n-1)*n:1;
}
因为递归函数调用是与
n
相乘的,这意味着最后一个操作是乘法,而不是递归调用。但是,它甚至处于
-O1
级别:

recursion`f:
0x10000930:pushq%rbp
0x10000931:movq%rsp,%rbp
0x10000934:movl$0x1,%eax
0x10000939:testq%rdi,%rdi
0x10000093c:jle 0x1000094E
0x1000093E:nop
0x10000940:imulq%rdi,%rax
0x10000944:cmpq$0x1,%rdi
0x10000948:leaq-0x1(%rdi),%rdi
0x1000094C:jg 0x10000940
0x1000094E:popq%rbp
0x1000094F:retq
他们说:

因此,您的最终规则是足够正确的。但是,返回
n
*事实(n-1)
在尾部位置有一个操作!这是乘法
*
,这将是函数执行的最后一项操作 在它回来之前。在某些语言中,这实际上可能是 实现为一个函数调用,然后可以是尾部调用 优化

然而,正如我们从ASM清单中看到的,乘法仍然是ASM指令,而不是单独的函数。所以我很难看出累加器方法的不同:

int fac_时间(int n,int acc){
返回(n==0)?acc:fac_次(n-1,acc*n);
}
整数阶乘(整数n){
返回fac_次(n,1);
}
这就产生了

recursion`fac_时间:
0x100008E0:pushq%rbp
0x1000008e1:movq%rsp,%rbp
0x100008e4:testl%edi,%edi
0x1000008e6:je 0x1000008f7
0x100008E8:nopl(%rax,%rax)
0x1000008f0:imull%edi,%esi
0x100008F3:decl%edi
0x100008F5:jne 0x100008F0
0x100008F7:移动%esi,%eax
0x1000008f9:popq%rbp
0x1000008fa:retq

我错过什么了吗?或者只是编译器变得更智能了?

正如您在汇编代码中看到的那样,编译器足够智能,可以将代码转换为基本等同于(忽略不同的数据类型)的循环:

GCC非常聪明,知道在整个递归调用序列中,对原始
f
的每次调用所需的状态可以保存在两个变量(
n
result
)中,因此不需要堆栈。它可以将
f
转换为
fac\u倍
,也可以将两者转换为
fac
。这很可能不仅是严格意义上的尾部调用优化的结果,而且是GCC用于优化的其他启发式负载之一


(由于我对这里使用的特定启发法了解不够,因此我无法更详细地介绍它们。)

非累加器
f
不是尾部递归的。编译器的选项包括通过转换将其转换为循环,或
call
/some insn/
ret
,但它们不包括没有其他转换的
jmp f

尾部调用优化适用于以下情况:

int ext(int a);
int foo(int x) { return ext(x); }
asm输出来自:

尾部调用优化意味着用
jmp
而不是
ret
保留函数(或递归)。其他任何东西都不是tailcall优化。不过,使用
jmp
优化的尾部递归实际上是一个循环


一个好的编译器会做进一步的转换,尽可能将条件分支放在循环的底部,从而删除无条件分支。(在asm中,循环的
do{}while()
风格是最自然的)。

无论“他们”是谁,他们都是错的。当然,
f(n-1)*n
就是
n*f(n-1)
。我不觉得这有什么奇怪的。gcc比“他们”更聪明@MSalters我对乘法可交换性并不感到惊讶,因为这是基本的数学规则。我的问题是,当实际结果不是递归函数调用结果,而是乘法时,为什么要对其进行优化(因为首先放置操作数,其中一个是递归调用,然后对它们进行乘法,即后缀表示法中的
af()*
)。这对我的同事来说也是一个惊喜。但是,如果在函数调用和常量之间进行数学运算,优化器的任务似乎很简单。引号的来源是什么,即“他们”是谁?对我来说,似乎优化器至少比你的源代码更聪明……正如我上面所说的,我对乘法的可交换性并不感到惊讶,因为它是基本的数学规则。我的问题是,当实际结果不是递归函数调用结果,而是乘法时,为什么要对其进行优化(因为首先放置操作数,其中一个是递归调用,然后对它们进行乘法,即后缀表示法中的
af()*
)。这对我的同事来说也是一个惊喜。但是,如果在函数调用和常量之间做了一个数学运算,那么对于优化器来说,这是一项简单的任务。@efpies在阅读了programmers.SE的文章之后,我想我明白了你的意思。重新措辞的答案,至少要明确这可能不仅仅是尾部调用优化的问题。
int ext(int a);
int foo(int x) { return ext(x); }
foo:                                    # @foo
        jmp     ext                     # TAILCALL