编译器如何很好地优化这个阶乘函数?

时间:2012-01-15 11:00:51

标签: gcc clang compiler-optimization

所以我一直在看一下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!

这让我真正意识到你的编译器通常可以比优化代码做得更好。但它提出了一个问题,如何做得如此出色??因此,任何人都可以解释(可能通过链接到相关代码)以下优化如何工作:

  1. 最初的foo优化是一个紧密循环。

  2. main的优化位置,直接返回结果,而不是实际执行foo

  3. 这个问题的另一个有趣的副作用是展示一些GCC / Clang可以做的更有趣的优化。

1 个答案:

答案 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;
    }
}

(当然,这不是手动编写函数最明智的方法。)