GCCの最適化がインテルコンパイラより賢くて驚いた話

はじめに

コンパイラの最適化機能が、どこまで何を見抜くかに興味がある。特に、あるコンパイラができて、他のコンパイラができなかったりすることが見つかると楽しい。そういうのを見つけたのでメモ。

ソース

コンパイラに食わせるコードはこんなの。

int func(int index){
  int a[1000] = {};
  return a[index];
}

配列に空のリストを渡すと、全てゼロクリアされる。なので、indexがどのような値であっても(配列の範囲内であるかぎり)この関数funcの返り値はゼロ。これをコンパイラが見抜けるか、という話。

コンパイラ

試したコンパイラ一覧。

  • gcc 5.1.0, 4.9.3, 4.8.5, 4.4.7
  • icc 16.0.1
  • clang Apple LLVM version 7.0.2

gccだけ無駄にいろいろバージョンがあったので全部試した。

結果

試したコンパイラの中で、gccだけがfuncの返り値が常にゼロであることを見ぬいた。gccは、最適化オプション-O1からこの関数がゼロであることを見抜く。吐くコードは、-O1の場合は

        movl    $0, %eax
        ret

-O2以上の場合は

        xorl    %eax, %eax
        ret

となる。ただし、古いバージョン(4.4.7)は見抜けず、ゼロクリアのためのmemsetをcallする。他のコンパイラ(インテルコンパイラとclang)は最適化オプションをいろいろ試したが、この関数の返り値が常にゼロであることを見抜けなかった。iccは__intel_avx_rep_memsetを、clangは___bzeroをcallする。

もうちょっと試す

どこまで教えたら他のコンパイラが見抜くかも調べてみる。

int func(){
  int a[1000] = {}; // gcc, clangで実行されない
  return a[0];
}

これだと、funcに引数が無いので、常に定数を返すことがわかる。インテルコンパイラはこれでも配列のゼロクリアをやめなかったが、clangは常に定数であることを見ぬいてゼロクリアをやめ、ただ0を返す関数にしてしまった。

逆に、GCCがどこまで頑張るか試した。初期化を空リストじゃなくて、for文で回してみる。

int func(){
  int a[1000];
  int i;
  for(i=0;i<1000;i++)a[i] = 0; // 実行されない
  return a[0];
}

GCCはこれも見ぬく。しかし、初期化を0で無い値にするとダメだった。

int func(){
  int a[1000];
  int i;
  for(i=0;i<1000;i++)a[i] = 1; // 実行される
  return a[0];
}

また、同じゼロクリアでも、C++のautoを使ったループや、std::fillでも無理だった。

まとめ

どのコンパイラも、配列のゼロクリアとそれ以外の値での初期化は区別している。しかしGCCはさらに配列がゼロクリアされたことを認識し、その後の最適化に利用しているっぽい。

おまけ

intじゃなくてcharだとどうなるだろ、と思ってこんなコードを食わせた。

char func(){
  char a[1000];
  char i;
  for(i=0;i<1000;i++)a[i] = 1;
  return a[0];
}

gccはちゃんとこれが無限ループになることを見ぬいて(笑)こんなコードを吐いた。

.L2:
        jmp     .L2

clangもこれが無限ループになることを見ぬいた上、警告もしてくれたが、インテルコンパイラは無限ループを見抜けなかったようだ。