C3 和 C 中除以零的未定义行为如何影响编译优化?
结论
类似的优化也可能发生在 C 中。C 标准规定,整数除法的除数为零时会产生未定义行为。编译器可以据此进行优化,但不一定会生成 foo(); return true; 这样的代码。C11 委员会草案 N1570,第 6.5.5 节
示例来自 C3,其中用了 uint 和 true。下面改用 C 中常见的 unsigned int 来表达同一个问题。在 C 代码中使用 uint 时,需要由程序或头文件定义它。
unsigned int x = foo();
unsigned int z = 255 / x;
return x != 0;
为什么编译器可以返回 true
只看行为有定义的执行:代码要继续运行到 return,除法中的 x 就不能为零。因此,在这些执行中,x != 0 始终为真,编译器可以把返回值简化为 true(在 C 中也可以写成 1)。如果 x 为零,程序在整数除法处就已经触发未定义行为,标准不再要求它产生特定结果。
这不表示编译器一定会采用页面展示的变换。它也可以删除对后续代码没有影响的 z 赋值和除法,同时保留 return x != 0;。foo() 可能产生可观察的副作用,因此仍需保留这次调用。最终生成什么代码,取决于具体实现和优化过程。
为什么不只是删除除法
后续没有使用 z,所以编译器可以判断,计算结果不会对程序中行为有定义的执行产生可观察影响。删除这项计算是合理的优化。不过,这次除法也可能让编译器得知:在除法正常完成的执行中,x 不可能为零。优化器可以利用这一条件简化后面的判断。
这两种变换都可能成立。未定义行为不是结果已经规定好的运行时错误。程序一旦执行到未定义行为,语言标准就不再约束这次执行的结果。因此,不能要求编译器只删除除法,而让其他语句保持原样。
为什么编译器不一定报错
编译时,编译器通常无法知道 foo() 会返回什么,除数是否为零可能要等到运行时才能确定。静态分析或编译器警告有时能发现风险,但 C 不要求实现对每一种未定义行为都发出诊断。如果问题能在编译时发现,编译器可能会给出警告;是否报告取决于工具及其设置。
避免除数为零
执行除法前,先显式检查 x,并为零值定义程序需要的处理方式:
unsigned int foo(void);
int f(void) {
unsigned int x = foo();
if (x == 0) {
return 0;
}
unsigned int z = 255u / x;
(void)z;
return 1;
}
这里,x 为零时会明确返回 0;只有确认它非零后,程序才执行除法。如果后续逻辑需要商,就在检查后使用 z;如果商没有用途,这次计算可以直接删掉。
备注:内容仅供参考。
