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

使用全局函数的递归函数的GCC优化差异,c,gcc,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(){ prin

前几天,我在使用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));
}
现在,让我们在程序中引入一个全局变量,并使用“gcc-Ofast-o fib2 fib2.c”再次编译

#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));
}
新的全局变量没有做任何有意义的事情。然而,执行时间的差异是相当大的

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

谢谢 彼得

在我的机器上(
gcc(Ubuntu 5.2.1-22ubuntu2)5.2.1 20151010
)我得到了这个:
时间。/fib1 0,36s用户0,00s系统98%cpu 0364总计

time./fib2 0,20s用户0,00s系统98%cpu 0208总计

man gcc

-Ofast
无视严格的标准遵从性-Ofast支持所有O3优化。它还支持对所有标准兼容程序无效的优化。它打开-ffast math和Fortran特有的-fno保护parens和-fstack数组

不太安全的选择,让我们试试
-O2

时间。/fib1 0,38s用户0,00s系统99%cpu 0377总计

time./fib2 0,47s用户0,00s系统99%cpu 0470总计

我认为,一些积极的优化没有应用于
fib1
,而是应用于
fib2
。当我为
-O2
切换
-Ofast
时,一些优化没有应用于
fib2
,而是应用于
fib1

让我们试试
-O0

时间。/fib1 0,81s用户0,00s系统99%cpu 0812总计

time./fib2 0,81s用户0,00s系统99%cpu 0814总计

没有优化,它们是相等的。

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

我相信您找到了一些非常聪明和奇怪的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' foog.s | wc
     33      66     297
我受过教育的(我以前见过很多次)猜猜看?Gcc试图变得聪明,在其热情中,理论上应该更容易优化的函数生成了更糟糕的代码,而对全局变量的写入使它变得足够混乱,以至于它无法如此努力地优化,从而生成更好的代码。这种情况经常发生,gcc(以及其他编译器,我们不要单独列出)使用的许多优化非常特定于他们使用的某些基准测试,在许多其他情况下可能无法生成运行速度更快的代码。事实上,根据我的经验,我只使用过-O2,除非我非常仔细地进行了基准测试,以确定-O3是有意义的。通常情况下,情况并非如此


如果您真的想进一步研究这一点,我建议您阅读gcc文档,了解哪些优化是通过
-O3
启用的,而不是
-O2
-O2
不这样做),然后一个接一个地尝试,直到你发现是哪一个导致了这种行为,优化应该是一个很好的提示。我正要这么做,但我没时间了(必须在圣诞节最后一分钟购物)。

这是因为第二个版本的早期版本中出现了内联限制。因为带有全局变量的版本做的更多。这强烈地表明,在这个特定示例中,内联使运行时性能更差

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


或者,只要用
\uuuu属性((noinline))

标记函数,我会怀疑的第一件事是可疑的基准测试方法。一个测量值不足以形成一个统计学家,他认为你需要更严格的基准测试。一个简单但容易改进的方法是修改代码,以便在循环中重复执行测量的代码。让循环在该循环中执行代码一百万次,然后比较这些结果。也要去掉printf语句。@KenClement:请等待50万秒,然后告诉我们结果。@KenClement他的观点是运行需要半秒才能运行100万次的代码是不可行的。由于是递归的,代码基本上已经重复运行了。我得到了与-O3相同的奇怪行为,不必是-Ofast。是的。我想@Peter可以看看>-O2不这样做看看我的答案,
-O2
只是交换
fib1
fib2
。只有
-O0
使它们相等。在我看来,它们必须相等,因为我们可以安全地排除全局变量-它的值从未被读取。您可以询问gcc哪些优化是使用
gcc-Q-O3--help=optimizers
启用的,或者使用
diff
获取差异列表。可能比筛选手册更容易,并且具有
$ cc -S -O3 -o foo.s -S foo.c
$ cc -S -DG -O3 -o foog.s -S foo.c
$ grep 'call.*f1' foo.s | wc
      2       4      18
$ grep 'call.*f1' foog.s | wc
     33      66     297