所以我一直在看一下GCC中O3
的一些魔法(实际上我正在使用Clang进行编译,但它与GCC相同,我猜测了很大一部分优化器被从GCC拉到Clang)。
考虑这个C程序:
int foo(int n) {
if (n == 0) return 1;
return n * foo(n-1);
}
int main() {
return foo(10);
}
我非常喜欢的第一件事(在这个问题中也是如此 - https://stackoverflow.com/a/414774/1068248)是int foo(int)
(基本因子函数)编译成紧密循环的方式。这是它的ARM程序集:
.globl _foo
.align 2
.code 16
.thumb_func _foo
_foo:
mov r1, r0
movs r0, #1
cbz r1, LBB0_2
LBB0_1:
muls r0, r1, r0
subs r1, #1
bne LBB0_1
LBB0_2:
bx lr
我认为,Blimey。这很有趣!完全紧密循环来做阶乘。哇。这不是尾调用优化,因为它不是尾调用。但它似乎做了类似的优化。
现在看看main
:
.globl _main
.align 2
.code 16
.thumb_func _main
_main:
movw r0, #24320
movt r0, #55
bx lr
说实话,这让我大吃一惊。它完全绕过foo
并返回3628800
10!
。
这让我真正意识到你的编译器通常可以比优化代码做得更好。但它提出了一个问题,如何做得如此出色??因此,任何人都可以解释(可能通过链接到相关代码)以下优化如何工作:
最初的foo
优化是一个紧密循环。
main
的优化位置,直接返回结果,而不是实际执行foo
。
这个问题的另一个有趣的副作用是展示一些GCC / Clang可以做的更有趣的优化。
答案 0 :(得分:16)
如果使用gcc -O3 -fdump-tree-all
进行编译,则可以看到递归已转换为循环的第一个转储为foo.c.035t.tailr1
。这意味着处理其他尾调用的相同优化也处理这种稍微扩展的情况。 n * foo(...)
或n + foo(...)
形式的递归并不难以手动处理(见下文),并且由于可以准确描述如何,编译器可以自动执行该优化。
main
的优化要简单得多:内联可以将其转换为10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 * 1
,如果乘法的所有操作数都是常量,则可以在编译时执行乘法。
更新:以下是如何手动删除foo
的递归,这可以自动完成。我不是说这是GCC使用的方法,但它是一种现实的可能性。
首先,创建一个辅助函数。它的行为与foo(n)
完全相同,只是其结果乘以额外的参数f
。
int foo(int n)
{
return foo_helper(n, 1);
}
int foo_helper(int n, int f)
{
if (n == 0) return f * 1;
return f * n * foo(n-1);
}
然后,将foo
的递归调用转换为foo_helper
的递归调用,并依赖factor参数来消除乘法。
int foo(int n)
{
return foo_helper(n, 1);
}
int foo_helper(int n, int f)
{
if (n == 0) return f;
return foo_helper(n-1, f * n);
}
将其转为循环:
int foo(int n)
{
return foo_helper(n, 1);
}
int foo_helper(int n, int f)
{
restart:
if (n == 0) return f;
{
int newn = n-1;
int newf = f * n;
n = newn;
f = newf;
goto restart;
}
}
最后,内联foo_helper
:
int foo(int n)
{
int f = 1;
restart:
if (n == 0) return f;
{
int newn = n-1;
int newf = f * n;
n = newn;
f = newf;
goto restart;
}
}
(当然,这不是手动编写函数最明智的方法。)