使用全局变量的递归函数中的GCC优化差异

时间:2015-12-23 13:04:59

标签: c gcc

有一天,我遇到了一个奇怪的问题,使用GCC和'-Ofast'优化标志。使用' gcc -Ofast -o fib1 fib1.c'编译以下程序。

#include <stdio.h>

int f1(int n) {
    if (n < 2) {
        return n;
    }
    int a, b;
    a = f1(n - 1);
    b = f1(n - 2);
    return a + b;
}

int main(){
    printf("%d", f1(40));
}

测量执行时间时,结果为:

peter@host ~ $ time ./fib1
102334155
real    0m0.511s
user    0m0.510s
sys     0m0.000s

现在让我们在程序中引入一个全局变量,然后使用&#39; gcc -Ofast -o fib2 fib2.c&#39;再次编译。

#include <stdio.h>

int global;

int f1(int n) {
    if (n < 2) {
        return n;
    }
    int a, b;
    a = f1(n - 1);
    b = f1(n - 2);

    global = 0;

    return a + b;
}

int main(){
    printf("%d", f1(40));
}

现在执行时间是:

peter@host ~ $ time ./fib2
102334155
real    0m0.265s
user    0m0.265s
sys     0m0.000s

新的全局变量没有做任何有意义的事情。但是,执行时间的差异很大。

除了问题(1)这种行为的原因是什么,如果(2)在不引入无意义变量的情况下可以实现最后的表现,也会很好。有什么建议吗?

由于 彼得

3 个答案:

答案 0 :(得分:1)

我相信你会遇到一些非常聪明且非常奇怪的gcc(mis - ?)优化。这就是我研究这个问题的时候。

我修改了您的代码,使其在全局范围内拥有#ifdef G

$ cc -O3 -o foo foo.c && time ./foo
102334155

real    0m0.634s
user    0m0.631s
sys     0m0.001s
$ cc -O3 -DG -o foo foo.c && time ./foo
102334155

real    0m0.365s
user    0m0.362s
sys     0m0.001s

所以我有相同的奇怪性能差异。

如有疑问,请阅读生成的汇编程序。

$ cc -S -O3 -o foo.s -S foo.c
$ cc -S -DG -O3 -o foog.s -S foo.c

这里变得非常奇怪。通常我可以很容易地遵循gcc生成的代码。在这里生成的代码是不可理解的。什么应该是非常简单的递归和添加应该适合15-20指令,gcc扩展到数百条指令,在堆栈上有一系列移位,加法,减法,比较,分支和大数组。看起来它试图将一个或两个递归部分转换为迭代,然后展开该循环。有一件事让我感到震惊,非全局函数只有一个递归调用(第二个是来自main的调用):

$ grep 'call.*f1' foo.s | wc
      2       4      18

虽然全球一个人有:

$ grep 'call.*f1' foog.s | wc
     33      66     297

我受过教育(之前我见过很多次)猜? Gcc试图变得聪明,并且在其热情中理论上应该更容易优化生成更糟糕代码的功能,而对全局变量的写入使其充分混淆,以至于无法进行优化以至于它导致更好的代码。这种情况一直都在发生,gcc(以及其他编译器也让它们不单独使用)的许多优化都非常特定于它们使用的某些基准测试,并且在许多其他情况下可能无法生成更快的运行代码。事实上,根据经验,我只使用-O2,除非我非常仔细地对事情进行基准测试,看看-O3是否有意义。它经常没有。

如果您真的想进一步研究这个问题,我建议您阅读gcc文档,了解哪些优化功能已通过-O3启用而不是-O2-O2不执行此操作) ,然后一个接一个地尝试它们,直到找到导致这种行为的那个,并且优化应该是一个很好的暗示正在发生的事情。我准备这样做,但我没时间(必须做最后一分钟的圣诞购物)。

答案 1 :(得分:0)

在我的机器上(gcc (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010)我得到了这个:
time ./fib1 0,36s user 0,00s system 98% cpu 0,364 total
time ./fib2 0,20s user 0,00s system 98% cpu 0,208 total

来自man gcc

  

-Ofast
  无视严格的标准合规性。 -Ofast启用所有-O3优化。它还支持对所有符合标准的程序无效的优化。它打开-ffast-math和Fortran特定的-fno-protect-parens和-fstack-arrays。

不太安全的选择,让我们试试-O2
time ./fib1 0,38s user 0,00s system 99% cpu 0,377 total
time ./fib2 0,47s user 0,00s system 99% cpu 0,470 total

我认为,某些积极的优化未应用于fib1,但已应用于fib2。当我为-Ofast切换-O2时,某些优化未应用于fib2,但已应用于fib1

让我们试试-O0
time ./fib1 0,81s user 0,00s system 99% cpu 0,812 total
time ./fib2 0,81s user 0,00s system 99% cpu 0,814 total

他们是平等的,没有优化 因此,在递归函数中引入全局变量一方面可以打破一些优化,另一方面可以改进其他优化。

答案 2 :(得分:0)

这是由第二版早期的内联限制引起的。因为带有全局变量的版本更多。这有力地表明,在这个特定的例子中,内联使得运行时性能更差。

使用-Ofast -fno-inline编译两个版本,时间差异消失了。事实上,没有全局变量的版本运行得更快。

或者,只需使用__attribute__((noinline))标记该功能。