当我使用g ++编译以下简单的递归代码时,汇编代码只会返回i,就好像g ++可以像人类一样做一些代数技巧。
int Identity(int i) {
if (i == 1)
return 1;
else
return Identity(i-1)+1;
}
我不认为此优化是关于尾递归的, 显然,g ++至少必须做以下两件事:
如何复制
% g++ -v
gcc version 8.2.1 20181127 (GCC)
% g++ a.cpp -c -O2 && objdump -d a.o
Disassembly of section .text:
0000000000000000 <_Z8Identityi>:
0: 89 f8 mov %edi,%eax
2: c3
更新: 感谢许多人回答了这个问题。我在这里收集了一些讨论和更新。
已更新+回答: 感谢下面的答案(我将其标记为有用的,并且还检查了manlio的答案),我想我知道编译器如何以一种简单的方式做到这一点。请参见下面的示例。 首先,现代gcc可以完成比尾递归更强大的功能, 因此代码被转换为如下形式:
// Equivalent to return i
int Identity_v2(int i) {
int ans = 0;
for (int i = x; i != 0; i--, ans++) {}
return ans;
}
// Equivalent to return i >= 0 ? i : 0
int Identity_v3(int x) {
int ans = 0;
for (int i = x; i >= 0; i--, ans++) {}
return ans;
}
(我猜想)编译器可以知道ans和我共享相同的 delta ,并且在退出循环时也知道i = 0。因此,编译器知道它应该返回i。
在v3中,我使用>=
运算符,因此编译器还会为我检查输入的符号。
这可能比我猜想的要简单得多。
答案 0 :(得分:7)
GCC的优化以GIMPLE格式对代码的中间表示进行传递。
使用-fdump-*
选项,您可以要求GCC输出树的中间状态并发现有关执行的优化的许多细节。
在这种情况下,有趣的文件是(数字可能会有所不同,具体取决于GCC版本)
这是起点:
int Identity(int) (int i)
{
int D.2330;
int D.2331;
int D.2332;
if (i == 1) goto <D.2328>; else goto <D.2329>;
<D.2328>:
D.2330 = 1;
return D.2330;
<D.2329>:
D.2331 = i + -1;
D.2332 = Identity (D.2331);
D.2330 = D.2332 + 1;
return D.2330;
}
最后一个提供递归的优化源:
int Identity(int) (int i)
{
int _1;
int _6;
int _8;
int _10;
<bb 2>:
if (i_3(D) == 1)
goto <bb 4>;
else
goto <bb 3>;
<bb 3>:
_6 = i_3(D) + -1;
_8 = Identity (_6);
_10 = _8 + 1;
<bb 4>:
# _1 = PHI <1(2), _10(3)>
return _1;
}
与SSA一样,GCC在必要的基本块开始处插入称为PHI
的伪函数,以合并变量的多个可能值。
这里:
# _1 = PHI <1(2), _10(3)>
其中_1
是1
还是_10
的值,这取决于我们是通过块2
还是块3
到达这里。 / p>
这是第一个将递归转换为循环的转储:
int Identity(int) (int i)
{
int _1;
int add_acc_4;
int _6;
int acc_tmp_8;
int add_acc_10;
<bb 2>:
# i_3 = PHI <i_9(D)(0), _6(3)>
# add_acc_4 = PHI <0(0), add_acc_10(3)>
if (i_3 == 1)
goto <bb 4>;
else
goto <bb 3>;
<bb 3>:
_6 = i_3 + -1;
add_acc_10 = add_acc_4 + 1;
goto <bb 2>;
<bb 4>:
# _1 = PHI <1(2)>
acc_tmp_8 = add_acc_4 + _1;
return acc_tmp_8;
}
处理尾部调用的同一优化还处理了通过创建累加器使尾部递归的琐碎情况。
https://github.com/gcc-mirror/gcc/blob/master/gcc/tree-tailcall.c文件的开始注释中有一个非常相似的示例:
该文件实现了尾递归消除。它也用来 通常分析尾调用,将结果传递到rtl级别 它们用于sibcall优化的地方。
除了标准的尾部递归消除外,我们还能处理最多 通过创建累加器使调用尾递归的简单案例。
例如以下功能
int sum (int n)
{
if (n > 0)
return n + sum (n - 1);
else
return 0;
}
被转化为
int sum (int n)
{
int acc = 0;
while (n > 0)
acc += n--;
return acc;
}
为此,我们维护了两个累加器(
a_acc
和m_acc
),它们指示 当我们到达return x语句时,我们应该返回a_acc + x * m_acc
代替。它们最初分别初始化为0
和1
, 因此该函数的语义显然得以保留。如果我们是 保证累加器的值永远不变,我们 省略累加器。在三种情况下函数可能如何退出。第一个是 在adjust_return_value中处理,其他两个在adjust_accumulator_values中处理 (第二种情况实际上是第三种情况的特例,我们 只是为了清楚起见,分开展示):
- 仅返回
x
,其中x
不会保留任何其他特殊形状。 我们将其重写为等效于returnm_acc * x + a_acc
的gimple。- return
f (...)
(其中f
是当前函数)被重写为 经典的尾递归消除方法,用于参数分配 并跳转到该功能的开头。累加器的价值 不变。- 返回
a + m * f(...)
,其中a
和m
不依赖于对f
的调用。 为了保留描述的语义,我们希望将其重写之前 这样我们终于回来了a_acc + (a + m * f(...)) * m_acc = (a_acc + a * m_acc) + (m * m_acc) * f(...)
。 即我们将a_acc
增加a * m_acc
,将m_acc
乘以m
,然后 消除对f
的尾部调用。特殊情况下,值只是 通过设置a = 0
或m = 1
获得加或乘的结果。
答案 1 :(得分:0)
gcc仍可以对递归进行优化。我想要搜索很多常见的递归模式,然后将其转换为迭代或封闭形式的对应模式。
您可能会读this good old short page关于(不是)众所周知的gcc优化事实。
答案 2 :(得分:0)
如果我们传递一个负值,那么原始代码将陷入无限循环,那么对于g ++消除此错误是否有效?
递增/递减有符号整数会导致溢出/流出,这是未定义的行为(与无符号整数不同)。编译器只是简单地假设UB不会在这里发生(即,除非您使用-fwrapv
,否则编译器始终假设有符号整数不会溢出/溢出)。如果是这样,那就是编程错误。