记一个奇怪的gcc编译优化:-ftree-vrp

记一个奇怪的gcc编译优化:-ftree-vrp

最近有同事遇到一个gcc不同编译优化选项结果不一致的问题,从该问题反映出编程规范(我更倾向于华为内部使用的”编程军规“的叫法)的问题非常有参考意义,在此分享给大家。

程序可简化如下:

#include 
 
int main() {
  int num = 0;
  int arr[5] = {10, 10, 10, 10, 10};
  while ((arr[4 - num] == 10) && (4 - num >= 0)) {
    num++;
    printf("while loop!\n");
  }
  printf("num = %d\n", num);
  int index = 4 - num;
  printf("index = %d\n", index);
  bool flag = index >= 0 ? 1 : 0;
  printf("flag = %d\n", flag);
  if (index >= 0) {
    printf("error!\n");
  }
  return 0;
}

当然这段代码有不少不符合编程军规的地方,这里先按下不表,下文再分析,这里先只看其功能。从代码逻辑来看,应该可以得到下面的打印结果:

while loop!
while loop!
while loop!
while loop!
while loop!
num = 5
index = -1
flag = 0

O0和O1编译( ”g++ hello.cpp -O1“ )后运行结果也确实如此,这符合我们的期望。但是选择O2编译( ”g++ hello.cpp -O2“ )后,却得到如下结果:

while loop!
while loop!
while loop!
while loop!
while loop!
num = 5
index = -1
flag = 1
error!

计算的 num index 一样,但基于 index 的范围判断却得到完全不同的结果,看上去似乎是O2编译优化出错了。想找到哪个优化pass导致这个问题倒也不难,我们可以尝试将gcc的O2比O1多的优化pass拿出来分别进行编译测试。这里列举了gcc各个优化等级所采用的优化pass。通过阅读O2的pass和实际测试,找到了肇事元凶。

-ftree-vrp

Perform Value Range Propagation on trees. This is similar to the constant propagation pass, but instead of values, ranges of values are >propagated. This allows the optimizers to remove unnecessary range checks like array bound checks and null pointer checks. This is >enabled by default at -O2 and higher. Null pointer check elimination is only done if -fdelete-null-pointer-checks is enabled.

使用 ”g++ hello.cpp -O1 -ftree-vrp“ 编译即可得到上面的错误结果,也即在O1的基础上加 " -ftree-vrp“ 优化pass。 " -ftree-vrp“ 的功能有点类似常量传播,但它传播的不是值,而是值的范围,也就是说编译器可以删除不必要的值范围检查,例如数组下标。

具体到我们这个问题,编译器认为 4 - num 是数组下标,是一个非负数,因而在13和15行地方不会真实的去判断 index 值的的范围,而直接认为它是非负的,从而得到错误的结果。这一点也可以直接从两种编译选项下( ”g++ hello.cpp -O1“ ”g++ hello.cpp -O1 -ftree-vrp“ )的反汇编得到印证:

记一个奇怪的gcc编译优化:-ftree-vrp_第1张图片
在加了 " -ftree-vrp“ 优化(右图)后,源码13行这里直接将数值 1 给了 flag,源码15行这里也省去了判断,直接调用打印函数。

此外,需要注意的是 " -ftree-vrp“ 的优化是传播的是值范围而不是值,因此源码11行 index 的计算没有问题。

讲道理,编译器优化要优先保证程序的正确性,其次才是提升性能。这个优化是否有点争议这里不好说,也没查到类似的资料。我们回到上面的代码,现在可以说一说这段代码的问题了。

  • 第一,在循环体内修改了循环变量,这个是编程军规明令禁止的。
  • 第二,访问数组时,没有先检查数组下标是否越界, while 循环的最后一次循环时,也即 index=5 的时候发生数组读越界,这里应该将两个条件调换一下顺序:
#include 
 
int main() {
  int num = 0;
  int arr[5] = {10, 10, 10, 10, 10};
  // while ((arr[4 - num] == 10) && (4 - num >= 0)) {
  while ((4 - num >= 0) && (arr[4 - num] == 10)) {
    num++;
    printf("while loop!\n");
  }
  printf("num = %d\n", num);
  int index = 4 - num;
  printf("index = %d\n", index);
  bool flag = index >= 0 ? 1 : 0;
  printf("flag = %d\n", flag);
  if (index >= 0) {
    printf("error!\n");
  }
  return 0;
}

根据短路原则,第一个条件不满足时,就不会再进行第二个条件判断了,也就不会发生数组读越界。并且这样修改后,使用 ”g++ hello.cpp -O1 -ftree-vrp“ 编译结果也是正确的:

while loop!
while loop!
while loop!
while loop!
while loop!
num = 5
index = -1
flag = 0

我的理解是这样的:因为第一个条件 (4 - num >= 0) 不满足,编译器就能发现 4 - num 已经小于 0 了。

从这个问题可以看出,编程军规的重要性,上面说的两个问题都是编程军规应该明令禁止的。对于一个团队,可能大家水平层次不齐,制定规范的编程军规供大家学习并要求执行,可以很好的杜绝这些问题。

最后,附上一个不错的cpp编程规范。

你可能感兴趣的:(开发调试工具,编译工具链,c++,开发语言,c,编译优化,调试)