0.1 + 1.1 == 1.2 は True である。

偏見
小数を扱う開発者であれば、一度はこの問題に出会ったことがあるのではないでしょうか。
多くの人が 0.1 + 1.1 == 1.2 は False だと言います。
では、少し質問を変えてもう一度お聞きします。
0.1 + 1.1 == 1.2 が False だというのは、本当に確かですか?
問題
本記事では Unity エンジンの C# を基準に説明します。
public class Test : MonoBehaviour
{
float a = 0.1f;
float b = 1.1f;
void Start()
{
float c = a + b;
Debug.Log(c == 1.2f); // result is true? false?
}
}少しスクロールを止めて、コードを見て答えてみてください。
上のコードで、Debug.Log(c == 1.2f) の出力は True と False のどちらになるでしょうか?
5秒ほど考えてみてください。
考えましたか?この内容をご存知の方には、5秒は長すぎたかもしれません。
では、実際に実行してみましょう。
Result is True
画像の下部を見ると、コンソールに True と表示されています。
少なくとも私は、float + float は正確に動作しないと教わったため、False だと予想していました。
では、コンピュータとそう教えてくれた人たち、どちらが間違っているのでしょうか?
記録方式
なぜこのようなことが起こるのかを理解するには、まずコンピュータがどのように小数を記録しているかを知る必要があります。
小数は float というデータ型で値を格納でき、4 Byte のサイズを持ちます。
float は下の図のように RAM 上で符号、指数部、仮数部という領域に分けて格納されます。
RAM (4 byte, 32 bit)
32 bit のうち 1 bit は符号に、8 bit は指数部に、残りの 23 bit は仮数部に使われます。符号は分かりますが、「指数部」と「仮数部」という難しい言葉を見ると、早くも頭が痛くなりそうです。
少しややこしいですが、順番にひとつずつ見ていきましょう。
具体例
11.25 という小数があります。この数が float にどのように格納されるか見ていきましょう。
1. 2進数への変換
現在 11.25 は10進数で表記されています。これを2進数に変換します。
2. 正規化
最も左にある 1 を基準にして、そのすぐ後に小数点を置き、移動した桁数分の指数を掛けます。
この過程を正規化 Normalization と呼び、前半部分の 1.01101 を仮数部、2³ を指数部と呼びます。
- 仮数部(Mantissa) : 実際の数値を保持する部分
1.01101 - 指数部(Exponent) : 仮数部にどれだけ掛けるかを決定する部分
2³
3. 値の格納
仮数部と指数部に分離したので、これをRAMに格納してみましょう。
最初の1ビットの符号は、正の数なら 0、負の数なら 1 と表記されます。
| フィールド | 計算値 | 格納値 (RAM) |
|---|---|---|
| 符号 (1 bit) | + |
0 |
| 指数部 (8 bit) | 3 |
10000010 |
| 仮数部 (23 bit) | 1.01101 |
01101000000000000000000 |
ちょっと待ってください!ここで読むのを止めて、もう一度表を確認してみてください。数字は正しく記入されていますか?
格納された仮数部をよく見ると、先頭の 1 が消えていることに気づきます。
そして指数部は、どう計算されたのかよく分かりませんが、10000010 という値で格納されています。
コンピュータはなぜこのように格納したのでしょうか?
指数部の Bias
指数部は小数点をどれだけ移動させるかを決定する部分だと説明しました。
小数点を右に移動させるには正の指数が、左に移動させるには負の指数が必要です。
8 bit で表現できる範囲は 0 ~ 255 です。
しかし負の指数も表現しなければならないのに、この範囲には負の数がありません。
そこで 127 を基準点として設定し、以下のように分けて使用します。
| 格納値 | 意味 |
|---|---|
0 |
特殊値 |
1 ~ 126 |
負の指数 |
127 |
0乗 |
128 ~ 254 |
正の指数 |
255 |
特殊値 (∞, NaN) |
0 と 255 は特殊値なので、今は気にしなくて大丈夫です。
重要なのは、実際の指数に127を加えた値 が指数部に格納されるということです。この基準値 127 を Bias と呼びます。
例を見ると、より分かりやすくなります。
先ほどの例で指数部に 10000010 が格納されていた理由がこれです。
3 + 127 = 130、そして 130 を2進数に変換すると 10000010 になります。
仮数部の Hidden Bit / Implicit Bit
今度は、仮数部が 1.01101 ではなく 01101 として格納された理由を見ていきましょう。
正規化の過程にはひとつのパターンがあります。正規化された2進数は 必ず 1. で始まる ということです。
先頭の 1 はどんな数を正規化しても必ず存在します。ならば、わざわざ格納する必要はないのではないでしょうか。
そのため、この 1. の部分は あるものとして約束 し、実際のメモリには小数点以下のみを格納します。
CPU が値を読み取る際に、自動的に先頭に 1. を付けて計算します。
この隠された 1 を Hidden Bit または Implicit Bit と呼びます。
おかげで仮数部は 23 bit ですが、隠された 1 を含めると実際には 24 bit の精度 を持ちます。
1 bit 分の精度をタダで得られるわけです。
計算
では、私たちが気になっている 0.1 + 1.1 は実際にはどのように計算されるのでしょうか?
10進数の 0.1 を2進数に変換すると、以下のように無限に繰り返す循環小数になります。
float の仮数部は 24 bit(Hidden Bit を含む)なので、24桁目で丸めて格納されます。
これを再び10進数に変換すると以下のようになります。
同様に、1.1 も正確には表現できません。
この2つを足すと以下のようになります。
しかし考えてみると、数直線上の数は無限に存在し、float はそのすべてを格納することはできません。そのため float は内部的に特定の値だけがあらかじめ決まっており、点のようにまばらに配置されています。
計算結果の 1.20000002533 は、float が決めたその点のひとつではないかもしれません。
例えば、C# の float では 1.20000004768 をその特定の値のひとつとしているとしましょう。
その場合、コンピュータは 1.20000002533 に最も近い点である 1.20000004768 を見つけて格納します。
この過程を Rounding(丸め) と呼びます。
そして 1.2f を float に格納する際にも、同じように丸めが行われます。
結局、2つの値は完全に同一です。
偶然の一致?
先ほど見たように、0.1f + 1.1f の Rounding 結果は 1.20000004768 です。
そして偶然にも、1.2f を float に格納する際の値も 1.20000004768 です。
整理すると以下のようになります。
0.1f + 1.1fの実際の計算結果は1.20000002533です。- この値が
Roundingされて、最も近いfloatの点1.20000004768になります。 1.2fの実際の格納値も1.20000004768です。- つまり
c == 1.2fは1.20000004768 == 1.20000004768なのでTrueです。
float a = 0.1f;
float b = 1.1f;
float c = a + b;
// c : 1.20000004768… (1.20000002533… が Rounding された)
// 1.2f : 1.20000004768… (float に格納された最も近い点)
Debug.Log(c == 1.2f); // Trueでは、このセクションのタイトルをもう一度読んでみてください。
これは本当に 偶然の一致 なのでしょうか?
IEEE 754
IEEE (Institute of Electrical and Electronics Engineers)
IEEE 浮動小数点演算規格(IEEE Standard for Floating-Point Arithmetic, IEEE 754)
ここまで見てきた float の動作は、バグでも偶然でもありません。
これらはすべて IEEE 754 という国際規格に定義された、意図的かつ論理的な動作です。
IEEE 754 は1985年に制定された浮動小数点演算の規格です。
32 bitをどのように分割するかBiasの値をいくらにするか- 丸めはどの方式で行うか(複数の方法がありますが、頭が痛くなるので省略します)
Hidden bitを使用するかどうか
世界の 99% がこの国際規格を使用しています。この規格の目的は、どのプログラミング言語でも同じ浮動小数点演算に対して同じ結果を保証することです。では、C#、Python、C++、JavaScript などの言語で float 演算を行えば同じ結果が得られ、0.1f + 1.1f == 1.2f はどの言語でも True になるのでしょうか?
残念ながら、そうではありません。規格に準拠していても、すべての環境で同じ結果が得られるわけではありません。JavaScript はすべての数値を double(64 bit)として扱うため、float(32 bit)を使用する C# とは精度そのものが異なります。また、コンパイラの最適化、CPU アーキテクチャ、ランタイム環境などによっても結果が変わることがあります。
stackoverflow によると、変数に格納するかどうかによっても結果が変わることがあるそうです。
式を直接比較すると False なのに、変数に格納してから比較すると True になる場合があるということです。
結論
もう一度お聞きします。
0.1 + 1.1 == 1.2 が False だというのは、本当に確かですか?
私が代わりに答えるとすれば、「一般的にはFalseで合っている」 と言うでしょう。
この記事で伝えたいのは、無条件に False と答えることが常に正しいロジックとは限らないということです。
人や教科書が教えてくれることを、たまには疑ってみる姿勢も悪くないと思います。
結論として、小数を扱う際は条件文に注意を払い、特に == 演算は避けるのが賢明です。
Result is false, But this may be a bias.
コンピュータにおける「Bias」は指数を正にすることを意味しますが、日常では「偏見」という意味です。