Stay Hungry.Stay Foolish.
一个有意思的编译器优化顺序争论

昨天看王垠的博客看到一个有意思的问题,代码如下

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

在老版本的Clang中开启O3优化之后,会变成

void contains_null_check_after_RNCE(int *P) {
  int dead = *P;
  if (P == 0)  // P 在上一行被访问,所以这里 P 不可能是 null
    return;
  *P = 4;
}
void contains_null_check_after_RNCE_and_DCE(int *P) {
  //int dead = *P;    // 死代码消除
  //if (P == 0)        // 死代码
  //  return;         // 死代码
  *P = 4;
}

优化结束,最后剩下

void contains_null_check_after_RNCE_and_DCE(int *P) {
  *P = 4;
}

我们可以发现上面这样优化代码,编译器做了两点优化

  1. 消除未使用的变量。
  2. 删除无用的 NULL 检测。

首先我觉得编译器优化应该不止是扫描优化一次,像O3这种会重复多趟进行优化消除无用代码,所以这里就存在一个优化1还是先优化2,两种不同的方式可以得到不同的结果。上面的例子毫无疑问先进行了2操作,如果我们先进行1操作呢?

void contains_null_check_after_DCE(int *P) {
  //int dead = *P;    // dead变量未使用,死代码消除
  if (P == 0)         
       return;          
  *P = 4;
}
void contains_null_check_after_DCE_and_RNCE(int *P) {
  if (P == 0)        // 保留空检测
    return;         // 保留
  *P = 4;
}

优化结束,最后变成

void contains_null_check_after_DCE_and_RNCE(int *P) {
  if (P == 0)        
     return;         
  *P = 4;
}

现在Clang新版本已经修复,采用了第二种优化顺序,和GCC保持一致,得到的结果都是

void contains_null_check_after_DCE_and_RNCE(int *P) {
  if (P == 0)        
     return;         
  *P = 4;
}

如果我们修改一下代码,修改为下面这样

int contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return -1;
  *P = 4;
  return dead;
}

编译器就会进行无用NULL检测消除了

int contains_null_check(int *P) {
  int dead = *P; //被用作返回值,保留
  if (P == 0)   //上面被解引用了,不可能为空,消除
    return -1;   //消除
  *P = 4;
  return dead;
}

最后变成了

int contains_null_check(int *P) {
  int dead = *P;
  *P = 4;
  return dead;
}

如果用汇编来看会只剩下

mov eax, DWORD PTR [rdi]
mov DWORD PTR [rdi], 4
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证
评论

暂无评论~~