首页 文章

为什么不总是使用编译器优化?

提问于
浏览
33

One of the questions that I asked some time ago有未定义的行为,因此编译器优化实际上导致程序中断 .

但是如果代码中没有未定义的行为,那么有没有理由不使用编译器优化?我理解有时,出于调试目的,可能不需要优化代码(如果我错了请纠正我) . 除此之外,在 生产环境 代码上,为什么不总是使用编译器优化?

另外,是否有理由使用 -O 而不是 -O2-O3

9 回答

  • 28

    原因是您开发了一个应用程序(调试版本),并且您的客户运行完全不同的应用程序(发布版本) . 如果测试资源很少和/或使用的编译器不是很流行,我会禁用发布版本的优化 .

    MS在其MSVC x86编译器中发布了许多优化错误的修补程序 . 幸运的是,我从未在现实生活中遇到过一个 . 但其他编译器并非如此 . MS Embedded Visual C中的SH4编译器非常错误 .

  • 0

    一个例子是短路布尔评估 . 就像是:

    if (someFunc() && otherFunc()) {
      ...
    }
    

    “智能”编译器可能会意识到someFunc将始终因某种原因返回false,使整个语句的计算结果为false,并决定不调用otherFunc来节省CPU时间 . 但是如果otherFunc包含一些直接影响程序执行的代码(可能会重置全局标志或其他东西),它现在将不执行该步骤并且您的程序进入未知状态 .

  • -10

    如果没有未定义的行为,但是存在明确的破坏行为(确定性正常错误,或者像竞争条件一样不确定), it pays to turn off optimization so you can step through your code with a debugger.

    通常情况下,当我达到这种状态时,我喜欢组合:

    • debug build (无优化)并逐步执行代码

    • sprinkled diagnostic statements 到stderr所以我可以轻松跟踪运行路径

    如果错误更加狡猾,我会拔出 valgrinddrd ,并根据需要添加 unit-tests ,以便隔离问题并确保在找到问题时,解决方案按预期工作 .

    在极少数情况下,调试代码有效,但发布代码失败 . 当这种情况发生时,几乎总是,问题出现在我的代码中;发布版本中的积极优化可以揭示由于对临时工具的生命周期误解等引起的错误......但即使在这种情况下,使用调试版本也有助于隔离问题 .

    简而言之,专业开发人员构建和测试调试(非优化)和发布(优化)二进制文件的原因有很多 . 恕我直言,同时拥有调试和发布版本的单元测试将为您节省大量的调试时间 .

  • 8

    有一个例子,为什么 sometimes 使用优化标志是危险的,我们的测试应该覆盖大部分代码以注意到这样的错误 .

    使用 clang (因为在gcc中甚至没有优化标志,进行一些iptimizations并且输出已损坏):

    File: a.cpp

    #include <stdio.h>
    
    int puts(const char *str) {
        fputs("Hello, world!\n", stdout);
        return 1;
    }
    
    int main() {
        printf("Goodbye!\n");
        return 0;
    }
    

    没有-Ox标志:

    clang --output withoutOptimization a.cpp; ./withoutOptimization>再见!

    使用-Ox标志:

    clang --output withO1 -O1 a.cpp; ./withO1>你好,世界!

  • 2

    3原因

    • 有时会混淆调试器

    • 它与某些代码模式不兼容

    • 不值得:缓慢或错误,或占用太多内存,或产生太大的代码 .

    在第2种情况下,想象一些故意更改指针类型的操作系统代码 . 优化器可以假设无法引用错误类型的对象,并生成代码来更改寄存器中更改的内存值并得到"wrong" 1答案 .

    案例3是一个有趣的问题 . 有时优化程序会使代码变小,但有时会使代码更大 . 大多数程序都不受CPU限制,即使对于那些程序来说,只有10%或更少的代码实际上是计算密集型的 . 如果优化器有任何缺点,那么只有不到10%的程序才能获胜 .

    如果生成的代码较大,则它对缓存不太友好 . 对于在微小环路中具有O(n3)算法的矩阵代数库,这可能是值得的 . 但是对于具有更典型时间复杂度的东西,溢出缓存实际上可能会使程序变慢 . 优化器可以针对所有这些东西进行调整,通常情况下,但是如果程序是一个Web应用程序,那么,如果编译器只执行通用的东西并允许开发人员不打开它,那么它肯定会更加开发人员友好 . 花哨的技巧Pandora's box.


    1.此类程序通常不符合标准,因此优化程序在技术上“正确”,但仍然没有按照开发人员的意图执行 .

  • 3

    编译器优化有两个缺点:

    • Optimisations几乎总是重新排列和/或删除代码 . 这将降低调试器的有效性,因为源代码和生成的代码之间不再存在1对1的对应关系 . 堆栈的某些部分可能会丢失,并且逐步执行指令可能会以违反直觉的方式跳过部分代码 .

    • 优化通常很昂贵,因此在启用优化的情况下编译需要更长的时间才能进行编译 . 在编译代码时很难做任何有效的工作,因此显然缩短编译时间是件好事 .

    -O3执行的某些优化可能会导致更大的可执行文件 . 这个在某些 生产环境 代码中可能不合适 .

    不使用优化的另一个原因是您使用的编译器可能包含仅在执行优化时才存在的错误 . 没有优化的编译可以避免这些错误 . 如果您的编译器确实包含错误,则更好的选择可能是报告/修复这些错误,更改为更好的编译器,或编写完全避免这些错误的代码 .

    如果您希望能够对已发布的 生产环境 代码执行调试,那么不优化代码也是一个好主意 .

  • 2

    我看到的两个重要原因来自浮点数学和过于激进的内联 . 前者是由于C标准极其严格地定义了浮点数学 . 许多处理器使用80位精度执行计算,例如,当值被放回主存储器时,仅下降到64位 . 如果一个例程的版本经常将该值刷新到内存,而另一个版本只在结束时抓取一次值,则计算结果可能略有不同 . 只是调整该例程的优化可能比重构代码对差异更强大更好 .

    内联可能会有问题,因为从本质上讲,它通常会导致更大的目标文件 . 也许这种增加是代码大小因实际原因而无法接受:例如,它需要适合具有有限内存的设备 . 或者代码大小的增加可能导致代码变慢 . 如果一个例程变得足够大以至于它不再适合缓存,那么最终的缓存未命中可能会快速超过首先提供的内联优势 .

    我经常听到那些在多线程环境中工作时会关闭调试并立即遇到大量新bug的人,因为新发现的竞争条件等等 . 优化器刚刚在这里透露了底层的错误代码,因此将其关闭以作出回应可能是不明智的 .

  • 2

    简单 . 编译器优化错误 .

  • 23

    刚发生在我身上 . 由swig生成的用于连接Java的代码是正确的,但在gcc上不能与-O2一起使用 .

相关问题