質問
次のコードをテストします。
#include <stdio.h>
#include <stdlib.h>
main()
{
const char *yytext="0";
const float f=(float)atof(yytext);
size_t t = *((size_t*)&f);
printf("t should be 0 but is %d\n", t);
}
次のようにコンパイルします。
gcc -O3 test.c
良好な出力は次のようになります。
"t should be 0 but is 0"
しかし、私の gcc 4.1.3 では次のようになります。
"t should be 0 but is -1209357172"
解決
コンパイラ フラグ -fno-strict-aliasing を使用します。
厳密なエイリアシングが有効になっている場合 (デフォルトでは少なくとも -O3 であるため)、次の行で:
size_t t = *((size_t*)&f);
コンパイラは、size_t* が float* と同じメモリ領域を指していないと想定します。私の知る限り、これは標準に準拠した動作です (Thomas Kammeyer が指摘したように、ANSI 標準の厳密なエイリアス規則の遵守は gcc-4 あたりから始まります)。
私の記憶が正しければ、char* への中間キャストを使用してこれを回避できます。(コンパイラは char* が何でもエイリアスできると想定しています)
言い換えれば、これを試してみてください(今は自分でテストできませんが、うまくいくと思います)。
size_t t = *((size_t*)(char*)&f);
他のヒント
C99 標準では、これは 6.5-7 の次のルールでカバーされます。
オブジェクトは、次のタイプのいずれかを持つLValue式の式によってのみアクセスされる保存値を持つものとします。73)
オブジェクトの有効な型と互換性のある型、
オブジェクトの有効な型と互換性のある型の修飾されたバージョン、
オブジェクトの有効なタイプに対応する署名型または符号なしタイプであるタイプ、
オブジェクトの効果的なタイプの適格なバージョンに対応する署名付きまたは署名されていないタイプであるタイプ、
メンバー間の前述のタイプの1つ(再帰的に、サブアグレージ酸組合のメンバーを含む)を含む集約または組合タイプ、または
キャラクタータイプ。
最後の項目は、(char*) への最初のキャストが機能する理由です。
ポインタのエイリアシングに関する C99 ルールに従って、これは許可されなくなりました。2 つの異なるタイプのポインターがメモリ内の同じ場所を指すことはできません。この規則の例外は、void ポインターと char ポインターです。
したがって、size_t のポインターにキャストしているコードでは、コンパイラーはこれを無視することを選択できます。float 値を size_t として取得したい場合は、それを割り当てるだけで、float は次のようにキャストされます (丸められずに切り捨てられます)。
size_t サイズ = (size_t)(f);// これは機能します
これは一般にバグとして報告されますが、実際にはオプティマイザがより効率的に動作できるようにする機能です。
gcc では、コンパイラ スイッチを使用してこれを無効にすることができます。私は -fno_strict_aliasing を信じています。
それは悪いCコードです:-)
問題となるのは、float 型の 1 つのオブジェクトに、それを整数ポインターにキャストして逆参照することによってアクセスすることです。
これはエイリアス規則に違反します。コンパイラは、float や int などの異なる型へのポインタがメモリ内で重複しないと自由に想定します。まさにそれをやってのけたのです。
コンパイラは、あなたが何かを計算し、それを float f に格納し、それ以上アクセスしないことを認識します。おそらく、コンパイラがコードの一部を削除し、割り当てが行われていない可能性があります。
この場合、size_t ポインタを介した参照解除により、スタックから初期化されていないガベージが返されます。
これを回避するには、次の 2 つのことを行うことができます。
float と size_t メンバーの共用体を使用し、型のパニングを介してキャストを行います。良くはありませんが、機能します。
memcopy を使用して f の内容を size_t にコピーします。コンパイラは、このケースを検出して最適化するのに十分な機能を備えています。
なぜ t は 0 でなければならないと考えるのでしょうか?
あるいは、より正確に表現すると、「浮動小数点ゼロの 2 進表現が整数 0 の 2 進表現と同じになるとなぜ思うのですか?」
これは間違った C コードです。キャストは C のエイリアス規則を破り、オプティマイザーはこのコードを破るようなことを自由に実行できます。おそらく、GCC が浮動小数点書き込みの前に size_t 読み取りをスケジュールしていることがわかります (FP パイプラインの遅延を隠すため)。
-fno-strict-aliasing スイッチを設定するか、union または reinterpret_cast を使用して、標準に準拠した方法で値を再解釈できます。
ポインターの位置合わせは別として、sizeof(size_t)==sizeof(float) が期待されています。私はそうではないと思います(64ビットLinuxではsize_tは64ビットである必要がありますが、浮動小数点数は32ビットです)。つまり、コードは初期化されていないものを読み取ることになります。
-O3 は「正常」とはみなされません。-O2 は、おそらく一部のマルチメディア アプリを除いて、一般的に上限のしきい値です。
一部のアプリはそこまではできず、 -O1 を超えると停止します。
十分に新しい GCC (ここでは 4.3 を使用しています) をお持ちの場合は、このコマンドがサポートされる可能性があります。
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
注意していれば、そのリストを調べて、このバグの原因となっている有効化している特定の最適化を見つけることができる可能性があります。
から man gcc
:
The output is sensitive to the effects of previous command line options, so for example it is possible to find out which
optimizations are enabled at -O2 by using:
-O2 --help=optimizers
Alternatively you can discover which binary optimizations are enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts | grep enabled
コードを次のようにテストしました。「i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc.ビルド 5465)」
問題はありませんでした。出力:
t should be 0 but is 0
したがって、コードにはバグはありません。それはそれが良いコードであるという意味ではありません。しかし、メイン機能のreturnTypeと「return 0;」を追加します。関数の終わりに。