cast の怪

T 研のデフォルトが LiL だと思ったら大間違いだ。演習3の時からずっと思っていたが、むしろ C++。構造化データ+大規模反復計算となると、ある意味必然かもしれない。「ふつうに、./configure; make; make install すればつかえます、10 GB くらいメモリがあれば」。いったいどこがふつうなのか。

今日ちょっと出た話題。gcc でポインタをキャストしてから代入すると、コンパイラで挙動が変わって困ったなという話。ポインタをキャストして代入してもとの型に戻すって、それこそなにやってるんだという話ですが、いやだからといって挙動が変わるって、いやしかしそんなアホな。

  float n;
  *((int*)&n) = 3;
  cout << n << endl;
  cout << *((int*)&n) << endl;

実際に問題のソースを見せてもらったわけではないんだけど、多分こういうことなんだろうと推測。ホントかなぁ・・・。こんなキャストが必要になってくるのは、某インタプリタ上での話なのだが。いわゆる C++コンパイラ作るのムズカシイよね、という話と同じ*1。だから、あれほど OCaml で(略。
実際に上のソースを g++ 4.0.3 でコンパイルしてみたら、確かに挙動が変わるのだ。

$ g++ -O1 -Wall cast.cc
$ ./a.out
4.2039e-45
3
$ g++ -O2 -Wall cast.cc
$ ./a.out
2.02129
3

というか、こういう操作はやめましょうよ、ねぇ。gcc のバグなのか、仕様が不定なのか。まぁ、ボクは使いませんからどっちでもいいですが・・・。

アセンブリではかせると状況が分かりました。代入前後のソースを比較すると、こんな感じ。まずは、-O1

        andl    $-16, %esp
        subl    $16, %esp
        movl    $3, -4(%ebp)
        flds    -4(%ebp)
        fstpl   4(%esp)
        movl    $_ZSt4cout, (%esp)
        call    _ZNSolsEd

movl $3, -4(&ebp) が代入部分。そのあとに、flds -4(%ebp) が多分スタック操作だと思うんだけど・・・。マニュアルに見あたらないのはドウシタモノカ。

これを -O2 にすると、これが困ったことになる。

        andl    $-16, %esp
        flds    -4(%ebp)
        subl    $16, %esp
        fstpl   4(%esp)
        movl    $3, -4(%ebp)
        movl    $_ZSt4cout, (%esp)
        call    _ZNSolsEd

あれ? flds と movl の前後関係が逆になってしまった。逆って、あんたそりゃ挙動変わりますよ。-O2 で吐いた、上の .s 中の flds と movl を入れ替えてコンパイルし直すと、やっぱり -O1 のときと同じ挙動になったから、この辺が問題っぽい。うーん、バグかなぁ。float として扱われるべきところに、int を代入しちゃっているわけで、型ごとに flow 解析して最適化しているなら、こういう不具合もあり得そうですねぇ。といっても、あとは想像の域を脱しませんが。てか、ちゃんと union 使ったら動きます。

*1:コンパイラの中間コード用に、どういうデータ構造が必要になるか考えれば、そのおぞましさはすぐに分かる