具有未定义行为的分支可以被假定为无法访问并优化为死代码?
考虑以下声明:
*((char*)NULL) = 0; //undefined behavior
它清楚地调用未定义的行为。 在给定的程序中是否存在这样的陈述意味着整个程序是不确定的,或者只有当控制流程达到这个陈述时行为才变得不确定?
如果用户从不输入数字3
,以下程序是否定义明确?
while (true) {
int num = ReadNumberFromConsole();
if (num == 3)
*((char*)NULL) = 0; //undefined behavior
}
或者,无论用户输入什么内容,它完全是未定义的行为?
另外,编译器能否假定在运行时永远不会执行未定义的行为? 这将允许及时推理:
int num = ReadNumberFromConsole();
if (num == 3) {
PrintToConsole(num);
*((char*)NULL) = 0; //undefined behavior
}
在这里,编译器可以推断,如果num == 3
我们将总是调用未定义的行为。 因此,这种情况一定是不可能的,并且该号码不需要被打印。 整个if
语句可以被优化。 按照标准是否允许这种倒退的推理?
在给定的程序中是否存在这样的陈述意味着整个程序是不确定的,或者只有当控制流程达到这个陈述时行为才变得不确定?
都不是。 第一个条件太强大,第二个条件太弱。
对象访问有时是按顺序排列的,但标准描述了程序在时间之外的行为。 丹维尔已经引用:
如果任何这样的执行包含未定义的操作,则该国际标准不要求执行该程序的实现具有该输入(甚至不涉及第一个未定义操作之前的操作)
这可以解释为:
如果程序的执行产生未定义的行为,那么整个程序具有未定义的行为。
所以,UB无法访问的语句不会给程序UB。 (由于输入的值)永远不会到达,因此不会给程序UB。 这就是为什么你的第一个条件太强大了。
现在,编译器通常无法知道UB是什么。 因此,为了允许优化器重新排列具有潜在UB的语句,如果它们的行为被定义,那么它将是可重新订购的,有必要允许UB“及时回溯”并且在前面的序列点之前出错(或者在C ++的术语,UB影响UB之前排序的东西)。 所以你的第二个条件太弱了。
一个主要的例子是优化器依赖于严格的别名。 严格别名规则的要点是允许编译器重新排序无法有效重新排序的操作,如果有问题的指针可能是同一个内存的话。 因此,如果您使用非法别名指针,并且UB确实发生,那么它可以很容易地影响UB语句之前的语句。 就抽象机器而言,UB声明尚未执行。 就实际的目标代码而言,它已被部分或完全执行。 但是该标准并没有试图详细说明优化器重新排序语句意味着什么,或者这对UB有什么影响。 它只是让执行许可证出现问题,只要它满意。
你可以认为这是“UB有时间机器”。
特别要回答你的例子:
PrintToConsole(3)
以某种方式知道肯定会返回,否则此示例不是候选项。 它可以抛出异常或其他。 与你的第二个类似的例子是gcc选项-fdelete-null-pointer-checks
,它可以采用这样的代码(我没有检查过这个具体的例子,认为它说明了一般的想法):
void foo(int *p) {
if (p) *p = 3;
std::cout << *p << 'n';
}
并将其更改为:
*p = 3;
std::cout << "3n";
为什么? 因为如果p
为null,那么代码无论如何都有UB,因此编译器可能会认为它不为null并相应地进行优化。 Linux内核绊倒了这个(https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897),主要是因为它在一种模式下运行,在这种模式下不需要引用空指针是UB,它预计会导致内核可以处理的定义的硬件异常。 当启用优化时,gcc需要使用-fno-delete-null-pointer-checks
来提供超越标准的保证。
PS对“未定义行为何时发生”这一问题的实际答案? 是“你计划在一天之前离开的10分钟”。
标准状态为1.9 / 4
[注:本国际标准对含有未定义行为的程序行为没有要求。 - 结束注释]
有趣的一点可能是“含有”意味着什么。 稍后在1.9 / 5它说:
然而,如果任何这样的执行包含未定义的操作,则本国际标准不要求执行该程序的实现具有该输入(甚至不涉及在第一个未定义操作之前的操作)
在这里它特别提到“执行......与那个输入”。 我将这样解释为:在一个可能的分支中未定义的行为现在不被执行,不会影响当前的执行分支。
然而,另一个问题是基于代码生成过程中未定义行为的假设。 请参阅Steve Jessop的答案,了解更多细节。
一个有启发意义的例子是
int foo(int x)
{
int a;
if (x)
return a;
return 0;
}
当前的GCC和当前的Clang都会优化这个(在x86上)
xorl %eax,%eax
ret
因为他们推断x
在if (x)
控制路径中始终为零。 GCC甚至不会给你一个使用未初始化值的警告! (因为应用上述逻辑的传递在生成未初始化值警告的传递之前运行)
上一篇: Can branches with undefined behavior be assumed unreachable and optimized as dead code?
下一篇: "Observable behaviour" and compiler freedom to eliminate/transform pieces c++ code