【问题标题】:GCC optimization differences in recursive functions using globals使用全局变量的递归函数中的 GCC 优化差异
【发布时间】:2015-12-23 13:04:59
【问题描述】:

前几天我在使用 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

现在让我们在程序中引入一个全局变量,并使用“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));
}

现在执行时间是:

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

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

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

谢谢 彼得

【问题讨论】:

  • 我首先会怀疑是可疑的基准测试方法。
  • 一次测量不足以形成统计数据
  • 我认为您需要更加严格地进行基准测试。一个简单但简单的改进是修改代码,以便它可以在循环中重复执行测量的代码。让循环在该循环中执行代码一百万次,然后比较这些结果。还要去掉 printf 语句。
  • @KenClement:请稍等一百万秒,然后告诉我们结果。
  • @KenClement 他的观点是,运行一百万次需要半秒钟的代码是不可行的。由于是递归的,代码基本上已经重复运行了。

标签: c gcc


【解决方案1】:

我相信您遇到了一些非常聪明和非常奇怪的 gcc(错误?)优化。这就是我研究这个的时候了。

我修改了您的代码以在全球范围内使用#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 不这样做),然后尝试它们直到您找到导致此行为的原因,并且该优化应该是对正在发生的事情的一个很好的提示。我正要这样做,但我没时间了(必须在最后一分钟做圣诞购物)。

【讨论】:

  • > -O2 不这样做看看我的回答,-O2 只是交换了fib1 和fib2。只有-O0 使它们相等。在我看来,它们必须相等,因为我们可以安全地排除全局变量——它的值永远不会被读取。
  • 您可以使用gcc -Q -O3 --help=optimizers 询问gcc 启用了哪些优化,或者使用diff 获取差异列表。可能比筛选手册更容易,并且具有与您正在使用的实际 gcc 版本相对应的优势,以防您的手册没有。
  • 感谢艺术。令人困惑和担心的是,第二个程序的速度几乎是 两倍。人们会期望“-Ofast”会为这两个程序提供或多或少相同的结果。实际上,我很想将这种优化行为视为错误。
  • @Peter 这可能是一个错误。或者它可能只是一种在某些情况下效果很好的优化,在这里效果不佳。我不想称它为错误,只是一个秘密工艺知识,即 gcc -O3 应用了一些可疑的优化,有时会使事情变得更糟。
  • @RomanZaytsev 在 gcc 版本上,我使用 -O2 和 -O3 之间的唯一区别是一条指令写入全局变量。我不想相信时机,因为我看到的差异非常接近噪音。
【解决方案2】:

在我的机器上 (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

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

【讨论】:

【解决方案3】:

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

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

或者,只需用__attribute__((noinline)) 标记函数。

【讨论】:

  • 实际上,它使两个程序的性能更差,尽管确实没有全局变量的程序更快。但是我们知道程序使用(无意义的)全局变量实际执行的速度有多快。那么问题来了,如何在不添加无意义的全局变量的情况下达到原来的(最好的)速度呢?
  • @Peter 在我的基准测试中,非内联版本的性能更快。
猜你喜欢
  • 1970-01-01
  • 2016-01-25
  • 2017-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-10
  • 2014-03-30
  • 2021-11-18
相关资源
最近更新 更多