はじめに
@mikecat_mixc さんの書かれた記事『A=B、A=Cであるとき、B=Cとなるとは限らない』への便乗です。
件の記事のコメント欄に記事の内容に則した Java プログラムの投稿がありました。それを割と素直に C に移植したものが以下となります。
#include <stdio.h>
int main(void)
{
const int A = 20000001;
const float B = A;
const double C = A;
if (A==B) puts("A==B");
if (A==C) puts("A==C");
if (B==C) puts("B==C");
return 0;
}
これの動作が少々厄介だというのがこの記事の趣旨です。
実行してみよう
とりあえず Wandbox で実行してみましょう。
https://wandbox.org/permlink/FNQMxoPYyWVnHFT2
A==B A==C
Java 版と同じ結果となりました。使用したコンパイラは gcc 9.1.0 です。
Wandbox は様々な版のコンパイラが使用できるので重宝します。現在使用できるコンパイラの中でも gcc 1.27 は特段に古いものです。GCC Releases を参照すると
September 5, 1988
とあるので C89 が規格化されるよりも前にリリースされたもののようです。折角なのでこれでも実行してみることにしましょう。
A==C
先とは異なる結果となりました。
どうしてこうなった
今回のプログラムは int は 32bit 以上であることを想定しています。
const int A = 20000001;
で、const int の変数 A に初期値として 20000001 を与えています。次に
const float B = A;
で、const float の変数 B に初期値として A の値を与えています。 A の値 20000001 は 2進数で表すと 1001100010010110100000001 であり、正確に表すには 25bit の幅を要します。IEEE 754 の単精度浮動小数点数の仮数部の幅は実質 24bit であり、それを採用された float に代入するには精度が足りません。仮数部に入らない下のビットが捨てられた場合、B には 20000000.0 が格納されます。
#include <stdio.h>
int main(void)
{
const int A = 20000001;
const float B = A;
printf("A = %d\n", A);
printf("B = %f\n", B);
return 0;
}
A = 20000001
B = 20000000.000000
C89 以前の K&R の仕様では、float の演算は先ず double に昇格された後行われる決まりでした。そのルールの上では
if (A==B) puts("A==B");
の A==B
の演算は、double に昇格された A の値 20000001.0 と double に昇格された B の値 20000000.0 の比較となるため一致はしません。結果、gcc 1.27 での実行結果ではA==B
は出力されませんでした。
対して C89 以降では、float の演算で double に昇格される決まりが実質なくなったので、
if (A==B) puts("A==B");
の A==B
の演算が float に昇格された A の値 20000000.0 と B の値 20000000.0 の比較で行われた場合、一致することとなります。gcc 9.1.0 での実行結果でA==B
が出力されたのはこの為でしょう。
x87 命令でも同様のことは起こりうる
現在の x86 CPU では SSE 命令により単精度浮動小数点数と倍精度浮動小数点数をそれぞれ異なる精度で計算することができます。それ以前に普及していた x87 命令では、浮動小数点演算は全て80bit の拡張倍精度浮動小数点数で行われていました。C89 以降の float 演算でも x87 命令では拡張倍精度浮動小数点数で行われるため、先の gcc 9.1.0 の例も x87 命令の使用を強制する `-mfpmath=387' オプションを指定してコンパイル、実行すると
A==C
A==B
の結果は一致しなくなります。
おわりに
おわりです。