C 编译器优化的 Bug
一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现诡异的行为。这使我想起以前见过的一个 GCC 的 bug。当时很多编译器专家认为那种做法是正确的,跟他们说不清楚。简言之,这种有问题的优化,喜欢利用 C 语言的“未定义行为”(undefined behavior)进行推断,最后得到奇怪的优化结果。
这类优化过程的推理方式都很类似,他们使用一种看似严密而巧妙的推理,例如:“现在有一个整数 x
,不知道是多少。但 x
出现在一个条件语句里面,如果 x > 1
,那么程序会进入未定义行为,所以我们可以断定 x
的值必然小于或者等于 1,所以现在我们利用 x ≤ 1
这个事实来对相关代码进行优化……”
看似合理,却是不正确的。举一个具体的例子吧。这篇 Chris Lattner 写于 2011 年的文章里面就含有这样一个优化。文中指出,编译器利用“未定义行为”进行优化,是合理的,对于性能是很重要的,并且举出这样一个例子:
void contains_null_check(int *P) {
int dead = *P;
if (P == 0)
return;
*P = 4;
}
这例子跟我之前看到的 GCC bug 不大一样,但大致是类似的推理方式:这个函数依次经过这样两个优化步骤(RNCE 和 DCE),之后得出“等价”的代码:
void contains_null_check_after_RNCE(int *P) {
int dead = *P;
if (false) // P在上一行被访问,所以这里P不可能是null
return;
*P = 4;
}
void contains_null_check_after_RNCE_and_DCE(int *P) {
// int dead = *P; // 死代码
// if (false) // 死代码
// return; // 死代码
*P = 4;
}
他的推理方式是这样:
- 首先,因为在
int dead = *P
里面,指针P
的地址被访问,如果程序顺利通过了这一行而没有出现未定义行为(比如当掉),那么之后P
就不可能是 null,所以我们可以把P == 0
优化为false
。 - 因为条件是
false
,所以整个 if 语句都是死代码,被删掉。 dead
变量赋值之后,没有被任何其它代码使用,所以对dead
的赋值是死代码,可以消去。
最后函数就只剩下一行代码 *P = 4
。然而经我分析,发现这个转换是错误的,而不只是像他说的“存在安全隐患”。现在我来考考你,你知道这为什么是错的吗?庆幸的是,现在如果你把这代码输入到 Clang,就算加上 -O3 选项,它也不会给你进行这个优化。这也许说明他们也许已经意识到了这个错误。
我写这篇文章的目的其实是想告诉你,不要盲目相信编译器的优化变换都是正确的。无论它看起来多么的合理,只要打开优化之后你的程序出现不合理的行为,你就不能排除编译器进行了错误优化的可能性。Lattner 指出这样的优化完全符合 C 语言的标准,但就算你符合国际标准,也有可能是错的。有时候你得相信自己的直觉……