Meh*_*dad -3 c++ optimization gcc undefined-behavior visual-c++
考虑一些使用未初始化堆栈变量的非常简单的代码(或更复杂的代码,请参见下文1),例如:
int main() { int x; return 17 / x; }
Run Code Online (Sandbox Code Playgroud)
这是 GCC 发出的 ( -O3):
mov eax, 17
xor ecx, ecx
cdq
idiv ecx
ret
Run Code Online (Sandbox Code Playgroud)
以下是 MSVC 发出的内容 ( -O2):
mov eax, 17
cdq
idiv DWORD PTR [rsp]
ret 0
Run Code Online (Sandbox Code Playgroud)
作为参考,以下是 Clang 发出的内容 ( -O3):
ret
Run Code Online (Sandbox Code Playgroud)
问题是,所有三个编译器都检测到这是一个未初始化的变量就好了 ( -Wall),但实际上只有一个编译器基于它执行任何优化。
这让我有点难堪……我认为几十年来为未定义行为而争论的所有内容都是为了允许编译器优化,但我看到只有一个编译器关心优化甚至是最基本的 UB 情况。
为什么是这样?如果我想要Clang 以外的编译器优化 UB 的这种情况,我该怎么办?有什么方法可以让我真正获得 UB 的好处,而不仅仅是任何一个编译器的缺点?
1显然,对于某些人来说,这对于SSCCE来说太过分了,无法理解实际问题。如果你想要一个更复杂的这个问题的例子,在程序的每次执行中都不是未定义的,只需稍微按摩一下。例如:
int main(int argc, char *[])
{
int x;
if (argc) { x = 100 + (argc * argc + x); }
return x;
}
Run Code Online (Sandbox Code Playgroud)
在海湾合作委员会你得到:
main:
xor eax, eax
test edi, edi
je .L1
imul edi, edi
lea eax, [rdi+100]
.L1:
ret
Run Code Online (Sandbox Code Playgroud)
在 Clang 你得到:
main:
ret
Run Code Online (Sandbox Code Playgroud)
同样的问题,只是更复杂。
Yak*_*ont 10
优化实际读取未初始化的数据并不是重点。
优化假设您读取的数据必须已初始化。
因此,如果您有一些只能写入 3 或 1 的变量,编译器可以假设它是奇数。
或者,如果将正符号常量添加到有符号值,我们可以假设结果大于原始有符号值(这会使某些循环更快)。
当优化器证明读取了未初始化的值时,它会做什么并不重要;使 UB 或不确定值计算更快不是重点。表现良好的程序不会故意这样做,花费精力使其更快(或更慢,或关心)是在浪费编译器编写者的时间。
它可能会因其他努力而失败。或者它可能不会。
考虑这个例子:
int foo(bool x) {
int y;
if (x) y = 3;
return y;
}
Run Code Online (Sandbox Code Playgroud)
Gcc 意识到函数可以返回明确定义的东西的唯一方法是 when xis true。因此,当优化打开时,没有分支:
foo(bool):
mov eax, 3
ret
Run Code Online (Sandbox Code Playgroud)
调用foo(true)不是未定义的行为。调用foo(false)是未定义的行为。标准中没有任何内容指定为什么foo(false)返回3。标准中也没有任何要求foo(false)不返回的内容3。编译器不会优化具有未定义行为的代码,但编译器可以在没有 UB 的情况下优化代码(例如删除 foo 中的分支),因为没有指定存在 UB 时会发生什么。
如果我想要 Clang 以外的编译器优化 UB 的这种情况,我该怎么办?
编译器默认这样做。Gcc 在这方面与 Clang 没有什么不同。
在你的例子中
int main() { int x; return 17 / x; }
Run Code Online (Sandbox Code Playgroud)
没有遗漏的优化,因为它首先没有定义代码将做什么。
您的第二个示例可以被视为错失的优化机会。尽管如此:UB 为优化没有 UB 的代码提供了机会。这个想法不是你在代码中引入 UB 以获得优化。作为您的第二个示例可以(并且应该)重写为
int main(int argc, char *[])
{
int x = 100 + (argc * argc + x);
return x;
}
Run Code Online (Sandbox Code Playgroud)
实际上,gcc 不会在您的版本中删除分支,这在实践中并不是什么大问题。如果您不需要分支,则不必编写它只是为了期望编译器将其删除。