【发布时间】:2016-03-22 09:27:43
【问题描述】:
他们说,尾递归优化仅在调用就在函数返回之前有效。因此,他们将此代码作为 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
他们说:
因此,您的最终规则是足够正确的。但是,return
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
我错过了什么吗?还是只是编译器变得更聪明了?
【问题讨论】:
-
无论“他们”是谁,他们都错了。
-
当然,
f(n-1)*n是n*f(n-1)。我不认为这是一个惊喜。 -
gcc 比“他们”更聪明
-
@MSalters 我对乘法交换性并不感到惊讶,因为它是基本的数学规则。我的问题是,当实际结果不是递归函数调用结果,而是乘法时,为什么要进行优化(因为您首先放置操作数,其中一个是递归调用,然后对它们进行乘法运算,即后缀符号中的
a f() *)。这对我的同事来说也是一个惊喜。但是,如果在函数调用和 constant 之间进行数学运算,这对优化器来说似乎是一件容易的事。 -
引号的来源是什么,即“他们”是谁?对我来说,优化器似乎至少比你的源更聪明......
标签: c++ assembly optimization g++ tail-recursion