使用全局函数的递归函数的GCC优化差异
前几天,我在使用GCC和'-Ofast'优化标志时遇到了一个奇怪的问题。使用“gcc-Ofast-o fib1 fib1.c”编译下面的程序使用全局函数的递归函数的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
#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