不同的优化级别可以导致功能不同的代码吗?

Ker*_* SB 44 c c++ gcc compiler-optimization

我很好奇编译器在优化时的自由度.让我们将这个问题限制在GCC和C/C++(任何版本,任何标准版本):

是否有可能根据编译的优化级别编写行为不同的代码?

我想到的例子是在C++中的各种构造函数中打印不同的文本位,并根据副本是否被删除而获得差异(尽管我无法使这样的东西工作).

不允许计数时钟周期.如果你有一个非GCC编译器的例子,我也很好奇,但我无法检查它.C中的示例的奖励积分:-)

编辑:示例代码应该是标准兼容的,并且从一开始就不包含未定义的行为.

编辑2:已经有了一些很棒的答案!让我稍微了解一下:代码必须构成一个格式良好的程序并且符合标准,并且必须在每个优化级别编译为正确的,确定性的程序.(这不包括形状不规则的多线程代码中的竞争条件等.)我也理解浮点舍入可能会受到影响,但让我们对此进行折扣.

我只获得了800点声望,所以我认为我将在第一个完整的例子中赢得50点声望以符合这些条件的(精神); 25如果涉及滥用严格别名.(视某人向我展示如何向他人发送赏金.)

Rob*_*obᵩ 19

适用的C++标准部分是§1.9"程序执行".它的部分内容如下:

符合实现需要模拟(仅)抽象机器的可观察行为,如下所述....

执行格式良好的程序的一致实现应该产生与具有相同程序和相同输入的抽象机的相应实例的可能执行序列之一相同的可观察行为....

抽象机器的可观察行为是它对易失性数据的读写顺序以及对库I/O函数的调用....

所以,是的,代码在不同的优化级别上可能表现不同,但(假设所有级别都产生一致的编译器),但它们的行为不能明显不同.

编辑:请允许我更正我的结论:是的,只要每个行为与标准抽象机器的行为之一明显相同,代码在不同的优化级别上的行为可能会有所不同.

  • 注意"可能的执行序列之一".它不必是每个优化级别的*相同*.因此,例如,允许编译器在-O0处以一个顺序评估函数参数,在-O4处以不同的顺序评估函数参数.根据实际发生的顺序输出不同的程序是有效的,尽管它不是C标准所称的"严格符合程序".然后`printf("%d \n",CHAR_BIT)`不是严格符合的程序,因为输出是实现定义的. (8认同)
  • @Kerrek:如果赋值运算符`T :: operator =(const T&)`具有可观察的副作用,那么优化是非法的.如你所知,即使副本有副作用,也有一个特殊规则允许复制ctor elision.副本分配没有这样的规则.同样适用于默认构造,其可观察行为也不能被省略.但只要没有任何东西影响可观察行为,编译器就可以省去它想要的东西(如果`x`未使用,则根本不构造任何对象). (2认同)

BЈо*_*вић 15

是否有可能根据编译的优化级别编写行为不同的代码?

只有当你触发编译器的bug时.

编辑

此示例在gcc 4.5.2上的行为有所不同:

void foo(int i) {
  foo(i+1);
}

main() {
  foo(0);
}
Run Code Online (Sandbox Code Playgroud)

编译时-O0创建一个程序崩溃与分段错误.
编译时-O2会创建一个进入无限循环的程序.

  • 你的例子不是一个编译器错误,它可以很好地进行优化,因为gcc会进行尾调用优化,它会在没有优化的情况下崩溃,因为你的堆栈空间不足. (8认同)
  • 以上绝对不是编译器错误.听起来代码在两种情况下都能正常运行. (5认同)
  • @nos:OP没有说他正在寻找编译器错误. (3认同)
  • 不仅是你触发了一个bug.例如:`int x = printf("hello")+ printf("world");`它可以打印`helloworld`或`worldhello`,它可能*取决于是否打开优化.即优化可以改变行为*未指定*的事物. (2认同)
  • 我喜欢这个例子,因为它实际上展示了一个特定的优化*尾递归*。在第一种情况下,未应用优化,并且它通过递归调用自身来生成堆栈溢出,而在第二个示例中,优化器已将递归转换为循环......再次**伟大的**示例。 (2认同)

Den*_*ose 13

浮点计算是差异的成熟来源.根据各个操作的排序方式,您可以获得更多/更少的舍入错误.

根据内存访问的优化方式,不太安全的多线程代码也可能有不同的结果,但无论如何,这基本上都是代码中的错误.

正如您所提到的,当优化级别发生变化时,复制构造函数中的副作用可能会消失.


Ste*_*sop 10

好吧,通过提供一个具体的例子,我公然为赏金做游戏.我会把其他人的答案和我的评论放在一起.

出于不同优化级别的不同行为的目的,"优化级别A"应表示gcc -O0(我使用的是版本4.3.4,但它并不重要,我认为任何甚至模糊的最新版本都会显示我的差异之后),"优化级别B"应表示gcc -O0 -fno-elide-constructors.

代码很简单:

#include <iostream>

struct Foo {
    ~Foo() { std::cout << "~Foo\n"; }
};

int main() {
    Foo f = Foo();
}
Run Code Online (Sandbox Code Playgroud)

优化级别A的输出:

~Foo
Run Code Online (Sandbox Code Playgroud)

优化级别B的输出:

~Foo
~Foo
Run Code Online (Sandbox Code Playgroud)

代码完全合法,但由于复制构造函数elision,输出依赖于实现,特别是它对禁用复制ctor elision的gcc优化标志敏感.

请注意,一般而言,"优化"是指编译器转换,它可以更改未定义,未指定或实现定义的行为,但不会更改标准定义的行为.因此,满足您的标准的任何示例都必须是一个程序,其输出未指定或实现定义.在这种情况下,标准没有指明复制ctors是否被省略,我恰好幸运的是GCC在允许的情况下可靠地消除了它们,但是可以选择禁用它.


Jen*_*edt 8

对于C,几乎所有操作都在抽象机器中严格定义,并且只有在可观察结果与该抽象机器完全相同时才允许进行优化.想到的那条规则的例外情况:

  • 未定义的行为不必在不同的编译器运行或错误代码的执行之间保持一致
  • 浮点运算可能会导致不同的舍入
  • 可以按任何顺序计算函数调用的参数
  • 具有volatile限定类型的表达式可能会或可能不会仅因其副作用而被评估
  • 相同的const合格复合文字可能会或可能不会折叠到一个静态内存位置


nin*_*alj 5

根据标准,任何未定义行为都可以根据优化级别(或月相)改变其行为。

  • @Kerrek SB:未指定的行为怎么样?给定像“f(a(x), b(y))”这样的函数调用,我们知道“a(x)”在“b(y)”之前或之后进行计算,但不是同时进行。 (2认同)