现代编译器优化如何将递归转换为返回常数
How can modern compiler optimization convert recursion into returning a constant?
当我用g 编译以下简单递归代码时,汇编代码只会返回i,就好像g 可以像人类那样做一些代数技巧。
。int Identity(int i) {
if (i == 1)
return 1;
else
return Identity(i-1)+1;
}
我认为这种优化不是关于尾部递归,显然,G 至少必须做这两件事:
- 如果我们传递一个负值,则此代码将属于无限循环,因此对于G 消除此 bug ? ,它有效
- 虽然可以将所有值从1到INT_MAX进行枚举,然后G 可以说此功能应返回I,显然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
更新:感谢许多人回答了这个问题。我在这里收集了一些讨论和更新。
- 编译器使用一些方法来知道传递负值会导致UB。也许编译器使用相同的方法来执行代数技巧。
关于尾部递归:根据Wikipedia的说法,我以前的代码不是尾部递归形式。我尝试了尾部递归版本,而GCC在循环时生成了正确的。但是,它不能像我以前的代码一样返回我。- 有人指出,编译器可能会尝试"证明" F(x)= X,但我仍然不知道所使用的实际优化技术的名称。我对这样的优化的确切名称感兴趣,例如常见的子表达消除(CSE)或某种组合或其他组合。
更新 回答:多亏了下面的答案(我标记了它有用,还检查了Manlio的答案),我想我知道编译器如何以一种简单的方式来执行此操作。请参阅下面的示例。首先,现代海湾合作委员会可以做比尾随更强大的事情,因此,代码被转换为类似的东西:
// 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中,我使用>=
操作员,因此编译器还为我检查输入的符号。这可能比我猜到的要简单得多。
gcc的优化通过了代码的中间表示形式,以称为gimple的格式。
使用-fdump-*
选项,您可以要求GCC输出树的中间状态,并发现有关执行优化的许多详细信息。
在这种情况下,有趣的文件是(数字可能因GCC版本而有所不同):
.004T.gimple
这是起点:
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;
}
.038t.eipa_sra
呈现递归的最后一个优化来源:
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
的值,具体取决于我们是通过Block 2
还是块3
到达此处。
.039T.Tailr1
这是递归变成循环的第一个垃圾场:
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
) 当我们到达返回X语句时,我们应该返回a_acc + x * m_acc
反而。它们最初分别为0
和1
初始初始化 因此,显然保留了该功能的语义。如果是 确保累加器的价值永远不会改变,我们 省略累加器。有三种情况该功能如何退出。第一个是 在awad_return_value中处理,其他两个在awad_accumulator_values中 (第二种情况实际上是第三种情况,我们 仅仅为了清楚起见):
- 只需返回
x
,其中x
不在其余的特殊形状中。 我们将其重写为gimple等效于返回m_acc * x + a_acc
。- 返回
f (...)
,其中f
是当前功能,在一个 经典的尾声消除方式,分配参数 并跳到功能的开始。累加器的值 不变。- 返回
a + m * f(...)
,其中a
和m
不依赖于f
的呼叫。 为了保留在我们希望重写之前所描述的语义 以我们终于返回的方式a_acc + (a + m * f(...)) * m_acc = (a_acc + a * m_acc) + (m * m_acc) * f(...)
。 IE。我们通过a * m_acc
增加a_acc
,将m_acc
乘以m
和 消除对f
的尾巴调用。当价值仅为特殊情况 通过设置a = 0
或m = 1
添加或仅乘以乘以。
gcc即使在非尾巴回收调用的情况下,也能够对递归进行优化。我想搜索了许多常见的递归模式,然后翻译成其迭代或封闭形式。
您可以阅读有关(不)有关GCC的好旧页面。
如果我们传递一个负值,则原始代码将属于无限循环,因此对于G 消除此错误是否有效?
增加/减少签名的整数可能会导致溢出/不确定的行为(与无符号整数不同)。编译器只是假设UB在这里没有发生(即编译器始终假设签名的整数不会溢出/Undeflow,除非您使用-fwrapv
)。如果确实如此,那是一个编程错误。
- 将公共递归转换为尾递归,因为大型输入的堆栈溢出
- 使用递归函数 (c++) 将长字符串转换为整数时输出错误
- 如何转换多次调用自己的递归算法?
- 如何将这种递归解决方案转换为分而治之?
- 将多个非原始递归调用转换为迭代解决方案
- 递归地将给定字符串转换为它所表示的数字
- 尝试"复制"shared_ptr向上转换行为会导致复制构造函数上的无限递归(导致段错误)
- 如何将递归函数(具有两个基本情况)转换为迭代函数
- 转换循环以递归基本方法
- 我想将这些循环转换为递归.是否可以将这些循环转换为递归
- 将递归函数转换为迭代函数
- 使用递归前缀转换前缀转换
- 如何在C++中将基本嵌套循环转换为递归循环
- 现代编译器优化如何将递归转换为返回常数
- 如何将循环转换为递归
- 递归基转换时间复杂度分析
- 从 'int' 到 'int*' 的转换无效(尝试使用指针进行递归)
- 转换递归函数
- 转换递归函数并使其迭代
- C++ |需要帮助从 for 循环函数转换 - >递归函数