【问题标题】:GCC Why Non Run Optimization All Time?GCC 为什么总是非运行优化?
【发布时间】:2021-09-16 12:01:27
【问题描述】:

我用 C 编写了众所周知的交换函数,并使用 gcc S 观察了汇编输出,并再次做了同样的事情,但优化了 O2

差异很大,因为我只看到了 5 行而不是 20 行。

我的问题是,如果优化真的有帮助,那么不一直使用它的原因是什么?为什么我们非优化编译代码?

给业内人士一个额外的问题,当您在测试后发布程序的最终版本时,您是否在编译时进行了优化?

我正在回复您所有的 cmets,请阅读。

【问题讨论】:

  • 优化后的代码更难调试。此外,还可以选择不同类型的优化,因此编译器不会强制执行特定的优化。
  • @daniel,最近没有,但在过去的 30 年中我遇到了许多这样的错误。更糟糕的是,这些通常是heisenbugs,直到您意识到这是优化代码的问题(因为通常在没有优化的情况下进行调试)。
  • @daniel 让您的代码更快——谁不想这样做? 老实说,我大部分时间都不关心优化。如今,计算机的速度快得离谱。普通(未经过特别优化)代码对我来说几乎总是足够快。
  • @daniel 在 OS 或 Microsoft Word 等大型程序中 我并没有说没人关心优化。显然很多人关心优化。但是你说过,“谁不想要那个?”,好像你很明显每个人都必须关心优化——我在这里告诉你我不关心。
  • 回复。 “我正在回复您所有的 cmets,请阅读。”:任何与问题相关的内容都应编辑到问题中。任何构成答案的内容都应照此发布,任何人都不应除了您和您​​直接回复的任何人之外,还需要阅读 cmets。 SO 不是一个讨论论坛,最好使用 cmets 来开发问题并促进答案。不要实际提出或回答问题。

标签: c assembly gcc optimization compiler-optimization


【解决方案1】:

有几个原因。

1。编译耗时较长

对于小型甚至中型项目,这在今天很少成为问题。现代计算机非常快。如果需要五或十秒通常没关系。但对于较大的项目,它确实很重要。特别是如果构建过程没有正确设置。我记得当我试图在游戏中添加一个功能时The Battle for Wesnoth。编译花了大约十分钟。如果可以的话,很容易看出您希望将其缩短到五分钟或更少。

2。优化后的代码更难调试

它使代码更难调试的原因是调试器不会逐行运行程序。那只是一种错觉。这是一个可能存在问题的示例:

int main(void) {
    char str[] = "Hello, World!";
    
    int number_of_capital_letters = 0;

    for(int i=0; i<strlen(str); i++) {
        if(isupper(str[i]))
            number_of_capital_letters++;
    }

    printf("%s\n", str);

    // Outcommented for debugging reasons
    // printf("%d\n", number_of_capital_letters);
}

您启动调试器并想知道为什么它不跟踪number_of_capital_letters。然后您会发现,由于您已注释掉最后一个 printf 语句,因此该变量未用于任何可观察的行为,因此优化器将您的代码更改为:

int main(void) {
    puts("Hello, World!");
}

有人可能会争辩说,您只需关闭优化器即可进行调试构建。当cow is a sphere 时,这在世界上是正确的。但第三个原因是

3。有时错误只会出现在更高的优化级别。

假设您有一个庞大的代码库。当您升级编译器时,突然出现一个错误。当您删除优化时,它似乎消失了。这里有什么问题?好吧,这可能是优化器中的错误。但它也可能是代码中的一个错误,它在新版本的优化器中表现出来。很多时候,具有未定义行为的代码在经过优化编译的代码中表现不同。

那你是做什么的?您可以尝试找出错误是在优化器中还是在您的代码中。这可能是一项非常耗时的任务。让我们假设它是优化器中的一个错误。该怎么办?你可以降级你的编译器,这不是最优的,有几个原因。特别是如果它是一个开源项目。想象一下,下载源代码,然后运行构建脚本,并为找出问题所在而绞尽脑汁数小时,然后您在一些文档中看到(前提是作者记录了它),您需要特定编译器的特定版本。

让我们假设这是您的代码中的错误。理想的事情当然是修复它。但也许你没有这样做的资源。这次你还可以要求编译它的任何人使用特定编译器的某个版本。

但是,如果您可以只编辑一个 Makefile 并将 -O3 替换为 -O2,您可以清楚地看到,在时间不是无限资源的非理想世界中,有时这是一个可行的选择。如果运气不好,这样的错误可能需要一周的时间才能找到。或者更多。那是您可以在其他地方度过的时间。

以下是此类错误的示例:

#include <stdio.h>

int main(void) {
    char str[] = "Hello";
    str[5] = '!';
    puts(str);
}

当我使用 gcc 10.2 编译时,根据优化级别,我得到了不同的结果。

没有优化:

Hello!

优化:

Hello!`@

自己试试吧:

https://godbolt.org/z/5dcKKrEW1

https://godbolt.org/z/48bz5ae1d

在这里我找到了一个论坛帖子,其中调试版本有效但未发布:https://developer.apple.com/forums/thread/15112

4。有时错误只出现在较低的优化级别。

是的,这也可能发生。在这种情况下,如果您不太关心正确性,则可以增加优化。但是,如果您确实关心,这可能是一种查找错误的方法。如果您的代码在经过优化和不经过优化的情况下都能正确运行,那么与仅经过优化编译的情况相比,它更有可能不包含将来困扰您的错误。

我没有找到可行的示例,但理论上可能可行。

int main(void) {
    if(1/0) // Division by zero
        puts("An error has occurred");
    else
        puts("Everything is fine");
}

如果编译时没有优化,很有可能会崩溃。但优化器可能会假设未定义的行为(如除以零)永远不会发生,因此它将代码优化为:

int main(void) {
    puts("Everything is fine");
}

假设1/0 是某种错误检查,不太可能评估为真,因此您通常会假设程序打印“一切都很好”。在这里,优化器隐藏了一个错误。

5。优化器可能会生成一个更大的二进制文件,或者使用更多的内存。或者其他不受欢迎的东西。

这有时很重要。特别是在嵌入式系统中。通常(总是)-O0 生成非常大的代码,但您可能希望使用-Os(优化大小而不是速度)而不是-O3 来获得一个小的二进制文件。有时还可以获得更快的代码。见下文。

6。优化器可能会产生较慢的代码

是的,真的。这并不经常发生,但它可能会发生。 this question 中说明了一个相关但不等效的示例,其中编译器在优化可执行文件大小时生成的代码比速度更快。

【讨论】:

  • 此外,您可以将每个优化级别扩展到一组 -f 选项,然后将它们一分为二以找到导致问题的特定优化。当然,您应该首先使用-fsanitize=address,undefined 进行编译。
  • 1) Optimized code is harder to debug 回到我的观点 :) 调试时调试的是 C 代码而不是汇编代码。例如使用 Clion 并调试您的项目,那么这里的优化到底有什么意义?
  • 2) Without optimization: 有点不相关,但是为什么输出是 Hello!?当你写!在索引 5 中,您写在 null char 之上,因此在打印时无法知道在哪里停止...
  • @daniel 1) 我认为他们在评论部分解释得很好。这是您正在调试的程序集,但调试器尝试进行匹配。各种结果。
  • @daniel 2) 关键是,如果您有一个调用未定义行为的错误,那么在启用优化时代码行为不同是很常见的,这里就是这种情况。在实际软件中,这些错误也存在,但很难找到。如果它通过减少优化一步来起作用,为什么还要麻烦呢?
【解决方案2】:

我个人通常会开启优化。

我的理由是:

交付的代码是用优化构建的,因为我们需要——尤其是数字——性能。由于您无法发布未测试的内容,因此还必须优化测试版本。我想在开发过程中可以在不优化的情况下构建,但我不希望在发布测试之前有额外的时间来构建优化和测试。此外,性能有时是规范的一部分,因此必须使用优化的代码进行一些开发测试。

我不觉得使用调试器来优化代码非常困难。请注意,考虑到我主要编写的程序类型——没有用户界面和数字库的花哨过滤器——printf 和 valgrind(与优化代码一起工作)是我的首选工具。

在最近的 gcc 版本中,至少,在优化开启而不是关闭的情况下,产生了更多更好的诊断。

这与编程中的许多其他内容一样,当然会因环境而异。

【讨论】:

  • 还要注意优化不是二元选择。 GCC 和 clang 有 -Og-O1 用于“轻量级”优化,不需要太多额外的编译时间,但会进行寄存器分配,而不是总是在每个语句之后将所有内容(register 变量除外)溢出到它们的堆栈槽中. (somewhat necessary 支持 jump 到 GDB 中的另一个源代码行,或在任何断点处停止时修改任何非常量变量)
  • -Og 明确用于编译/测试/编辑周期,当您不想像默认的-O0 那样缓慢时,希望不会对可调试性造成太大影响。肯定小于-O3 -march=native -flto 使用 LTO 跨文件内联的自动矢量化。
【解决方案3】:

如果您从不使用源代码级调试器,您可能可以。但是,如果您从不使用源代码级调试器,那么您可能应该这样做。

未优化的代码与源代码中的语句表达式和变量具有直接的一一对应关系,因此在单步执行代码时,这一切都是有意义的 - 所有行都按照您期望的顺序执行,所有变量在您期望他们这样做时具有有效状态。

另一方面,优化的代码可以消除代码和变量,并重新排序执行,并且通常会使源代码级调试成为无稽之谈。有时您会遇到出现在优化构建中的错误,因此您可能必须处理它,但通常这些事情是未定义行为的结果,它是通常最好在一开始就避免这种情况。

需要考虑的一点是,在开发过程中,您已经对未优化的代码执行了所有测试和开发;所以你可以调试它。如果在你发布它的那天你启动了优化器并发布了它,那么你实际上是在发布大量未经测试的代码。测试很难,你真的应该测试你发布的东西,所以在构建和发布之间你可能需要做很多工作来消除风险。发布到您在整个开发过程中每天都在测试的相同构建规范可能会降低风险。

对于在桌面上运行的代码响应和等待用户输入,或者受哪些磁盘或网络 I/O 限制,使代码更快或更小通常没什么用。大型应用程序的某些特定部分可能会受益,例如大型数据集上的排序或搜索算法,或者图像或音频处理,对于那些您可能会使用目标而不是整个应用程序优化的部分。

在嵌入式系统中,您使用的处理器通常比具有更小的内存资源的桌面系统慢得多,因此速度和大小的优化可能很关键,但即使在嵌入式系统中,代码通常也必须适应并满足实时期限它的调试版本是为了支持测试和调试。如果只进行优化,调试起来会更加困难。

除了优化您的代码之外,也许应该注意的是,为了完成这项工作,优化器必须通过诸如抽象执行之类的技术对代码进行更深入的分析,并在做所以可以发现错误并发出正常编译无法检测到的警告。例如,优化器非常擅长检测可能在初始化之前使用的变量。为此,我建议将优化器作为一种“穷人的”静态分析来打开和最大化,即使您使用较低的优化级别进行发布 - 出于前面给出的原因。

优化器也是所有编译器中最复杂的部分;如果编译器将出现错误,则很可能在优化器中。也就是说,我只在 Microsoft C v6.0 1989 中遇到过一个这样的已确认错误!更常见的情况是,起初看起来是编译器错误,结果却是未定义的行为或正在编译的源代码中的潜在错误,这些错误通过不同的代码生成选项表现出来。

【讨论】:

【解决方案4】:

一个原因可能只是:传统。第一个 C 编译器是为 DEC PDP-11 编写的,它有 64k 地址空间。 (没错,那句著名但神话般的旧 IBM PC 引用的十分之一是关于“640k 应该对任何人都足够了”。)第一个 C 编译器作为相当多的单独程序或通道运行:有预处理器 cpp,解析器c0,代码生成器c1,汇编器as,以及链接器ld。如果您要求优化,它会作为单独的传递 c2 运行,这是一个在 c1 的输出上运行的“窥孔优化器”,然后将其传递给 as

当时的编译速度比现在慢得多(当然,处理器要慢得多)。人们并不经常要求对日常工作进行优化,因为它确实在您的编辑/编译/调试周期中花费了您很多东西。

尽管自那时以来发生了很多变化,但优化是一些额外的、特别的东西,你必须明确提出要求,这一事实仍然存在。

【讨论】:

    猜你喜欢
    • 2020-07-24
    • 2012-09-20
    • 1970-01-01
    • 2010-09-12
    • 2021-11-22
    • 1970-01-01
    • 2016-09-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多