type punning と strict aliasing

/.JGCC-3.4リリースの話題を追っていたら、GCC3では-O2で-fstrict-aliasingが有効だから注意せよというポスト(http://slashdot.jp/comments.pl?sid=175355&cid=537217)があった。

strict aliasing については、Radium Software Developmentさんが詳しい。これは気にしておいたほうがよさそうだ。GCCの -fstrict-aliasing の説明は次。

コンパイルされている言語に適用可能な別名規則(aliasing rule)のうち最も厳密なものをコンパイラが前提することを許します。これによって、 C(およびC++)では式の型に基く最適化を動作させることになります。例えば、ある型のオブジェクトが別の型のオブジェクトと同一アドレスに位置することは、それら2つの型がほとんど同一でない限り、ないものと仮定されます。例えば、 unsigned intがintの別名となることはあっても、 void*やdoubleの別名となることはありえません。また、文字型は他の任意の型の別名になりえます。 以下のようなコードに特に注意してください.

最後に書き込みが行われた共用体メンバとは異なるメンバから読み込みを行う習慣(「type-punning」と呼ばれる) は一般的に見られます。`-fstrict-aliasing'を指定した場合でも、メモリが共用体型を通してアクセスされる場合にはtype-punningは許されます。

例えば次のコードは手元の GCC3 + -O2で誤動作した。一部のGCC2でも誤動作する。しかし、どちらもコードがダメなのであってコンパイラは悪くない。

int add_something(int x) {
    ((short*)&x)[0] |= 1;  //  (A) 変数xのメモリにアクセス
    ((short*)&x)[1] |= 1;  //  (B) 変数xのメモリにアクセス
    return x;              //  (C) 結果をEAXレジスタにコピー
}

int main(void) {
    int result = add_something(0);
    std::printf("%d\n", result);
}

プログラマはresultが65537 (= 1<<16 + 1) になることを期待していると思うが、最適化をかけると出鱈目な値(1)が戻る。アセンブリリストを見ると、Bの代入よりもCの結果コピーのほうが先に来てしまっている。コンパイラは、コードが規格に合致していることを仮定した上で、速度向上のためにメモリアクセスのreorderを行ってしまうわけだ。


上のコード片など特に、うっかり書いてしまいそうな感じがして油断ができない。当面、GCC3系を使うときは-fno-strict-aliasingをデフォルトにしておこうと思う。


(追記)最近のgccには-Wstrict-aliasing=N というオプションがあります