お断り:ここでは非数と無限大については考慮していません。
IEEE-754 倍精度浮動小数点形式
Python の float や JavaScript の Number は IEEE-754 倍精度浮動小数点形式となっています。
符号部(S) | 指数部(E) | 小数部(T) |
---|---|---|
1 ビット | 11 ビット | 52 ビット |
式で表現すると
\left( -1 \right)^{S} \times 2 ^{E-1023} \times \left( 1 + \frac{ T }{ 2^{52} } \right)
になります。($E=0$の場合を除く)
浮動小数点演算器(FPU)の挙動
浮動小数点演算器(FPU)はどのような計算をしているのか考えます。
乗除算
指数部と小数部を考慮せず、符号部に注目すると
計算 | 被乗数 | 乗数 | 結果 |
---|---|---|---|
正 × 正 = 正 | 正(S=0) | 正(S=0) | 正(S=0) |
正 × 負 = 負 | 正(S=0) | 負(S=1) | 負(S=1) |
負 × 正 = 負 | 負(S=1) | 正(S=0) | 負(S=1) |
負 × 負 = 正 | 負(S=1) | 負(S=1) | 正(S=0) |
計算 | 被除数 | 除数 | 結果 |
正 ÷ 正 = 正 | 正(S=0) | 正(S=0) | 正(S=0) |
正 ÷ 負 = 負 | 正(S=0) | 負(S=1) | 負(S=1) |
負 ÷ 正 = 負 | 負(S=1) | 正(S=0) | 負(S=1) |
負 ÷ 負 = 正 | 負(S=1) | 負(S=1) | 正(S=0) |
となります。これは、符号部(1 ビット)の XOR で済みます。
結果が "0" のとき、符号部をどちらかに決めるとなると、0 を判定するために
- 判定回路を追加する
- 判定処理を追加する
などのコストが発生し、かつ演算性能が落ちる、というデメリットまで生じかねません。
だったら、+0 と -0 はそのままにしておく方が良いです。
加減算
加減算では、符号を整理すると「同符号の加算」と「同符号の減算」に分けることができます。
同符号の加算
乗除算同様に、以下のパターン
計算 | 被加減算数 | 加減算数 | 結果 | 同符号の加算 |
---|---|---|---|---|
正 + 正 = 正 | 正(S=0) | 正(S=0) | 正(S=0) | 正 + 正 |
正 - 負 = 正 | 正(S=0) | 負(S=1) | 正(S=0) | 正 + 正 |
負 - 正 = 負 | 負(S=1) | 正(S=0) | 負(S=1) | 負 + 負 |
負 + 負 = 負 | 負(S=1) | 負(S=1) | 負(S=1) | 負 + 負 |
では、符号部(1 ビット)だけの論理演算で済みます。これも乗除算と同様です。
同符号の減算
上記以外の加減算は
計算 | 被加減算数 | 加減算数 | 結果 | 同符号の減算 |
---|---|---|---|---|
正 - 正 = ? | 正(S=0) | 正(S=0) | ? | 正 - 正 |
正 + 負 = ? | 正(S=0) | 負(S=1) | ? | 正 - 正 |
負 + 正 = ? | 負(S=1) | 正(S=0) | ? | 負 - 負 |
負 - 負 = ? | 負(S=1) | 負(S=1) | ? | 負 - 負 |
となって、結果の数値に符号部は依存します。
(正 - 正)相当の計算のイメージは「(桁合わせをした)符号あり整数の減算」です。
(負 - 負)相当の計算は 「-(正 - 正)」として同様です。
結果は 0 未満が負の扱いなので、「0」は「+0」になります。
比較
FPU の比較命令(アセンブリ言語の話になります)にて「+0」と「-0」が同じ「0」なのに一致しないと判断されてはプログラムを組むのが大変になってしまいます。逆に、「+0」と「-0」が一致しないと判断する比較命令はなくても困りません。
すると、比較の基本は減算結果(「<:負」、「=:0」、「>:正」)なので、FPU の比較命令は「同符号の減算」相当の処理だけでよいことになります。
これは、FPU によっては "+0"と"-0"が一致しないと判断する比較機能がないかもしれず、その場合は "+0"と"-0"は区別しないことを意味します。
実験
以上を踏まえて、実験してみます。実行環境は macOS 12.1 (Intel版) です。
Python 3.8.9 (default, Oct 26 2021, 07:25:54)
[Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 1
>>> while a > 0: a /= 2
...
>>> a
0.0
>>> b = -1
>>> while b < 0: b /= 2
...
>>> b
-0.0
>>> a == b
True
>>> [a, b]
[0.0, -0.0]
>>> a * b
-0.0
>>> a + b
0.0
>>> b - a
-0.0
>>> b + b
-0.0
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> a = 1; while (a > 0) a /= 2; a
0
> b = -1; while (b < 0) b /= 2; b
-0
> a === b
true
> [a, b]
[ 0, -0 ]
> a * b
-0
> a + b
0
> b - a
-0
> b + b
-0
どの言語も FPU の機能を使っているハズなので予想通りというわけです。
こういう場合はあるのかな?
Y = X + 0; /* X が -0 だった場合に +0 に変換(X = Y にしちゃダメ) */
/* Y の符号で分岐... */