13

C 编译器优化的 Bug

 3 years ago
source link: http://www.yinwang.org/blog-cn/2016/10/12/compiler-bug
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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;
}

他的推理方式是这样:

  1. 首先,因为在 int dead = *P 里面,指针 P 的地址被访问,如果程序顺利通过了这一行而没有出现未定义行为(比如当掉),那么之后 P 就不可能是 null,所以我们可以把 P == 0 优化为 false
  2. 因为条件是 false,所以整个 if 语句都是死代码,被删掉。
  3. dead 变量赋值之后,没有被任何其它代码使用,所以对 dead 的赋值是死代码,可以消去。

最后函数就只剩下一行代码 *P = 4。然而经我分析,发现这个转换是错误的,而不只是像他说的“存在安全隐患”。现在我来考考你,你知道这为什么是错的吗?庆幸的是,现在如果你把这代码输入到 Clang,就算加上 -O3 选项,它也不会给你进行这个优化。这也许说明他们也许已经意识到了这个错误。

我写这篇文章的目的其实是想告诉你,不要盲目相信编译器的优化变换都是正确的。无论它看起来多么的合理,只要打开优化之后你的程序出现不合理的行为,你就不能排除编译器进行了错误优化的可能性。Lattner 指出这样的优化完全符合 C 语言的标准,但就算你符合国际标准,也有可能是错的。有时候你得相信自己的直觉……


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK