「0.1 + 0.2 ≠ 0.3」を解明せよ
こんにちは!ソーシャルデータバンク株式会社・開発部のくすみです。
いきなりですが、みなさん!
この数式の答えはなんでしょうか?
0.1 + 0.2 = ?
あなたが人間であれば 0.3 を頭に浮べてくれたと思います。
しかし、コンピュータの世界では 0.3 にはなりません。
実際にあなたの環境で console を叩いて確認してみましょう。
右クリックで「検証」を開いていただき、console タブを開いてください。
無事に開くことができたら、
console の中に0.1 + 0.2 === 0.3を入れてみましょう
0.1 + 0.2 === 0.3 の結果は false になりました…。
詳細を確認すると…
なんと!
0.1 + 0.2 は 0.30000000000000004 だそうです。
なぜ、0.1 + 0.2 がこのような結果になるのでしょうか?
その原因は...
10 進数における多くの少数は 2 進数に正しく変換できないから
この記事では、こちらの問題について深掘りしていきたいと思います!!!
この問題には浮動小数点数というものに関わってくるのですが、まずは 2 進数のおさらいから行っていきたいと思います。
2 進数の簡単なおさらい
まず、コンピュータが扱う最小単位というものとしてビット(bit)があります。
1 ビットで表現できる数字は、「0」と「1」の 2 つだけであり、
「0」と「1」という 2 個の数字で数をあらわすのが 2 進数です
- 私たち人間が普段から使用するのは 10 進数
- コンピュータが使用するのは 2 進数
では、ここで 10 進数を 2 進数に変換する変換クイズをやってみましょう〜
問題です。
10 進数の 15 を 2 進数に変換してください
10 進数の数を 2 進数に変換するには、変換したい10 進数を「2」で割りつづけて、答えが 「0」 になるまで余りを求めます。
求めた余りを下から並べた 1111が、2 進数で表した「15」になります。
では、この調子で 2 問目を解いてみましょう!
10 進数の 0.875 を 2 進数に変換してください
続いて小数の 10 進数「.875」を 2 進数に変換してみましょう。
小数の 10 進数を 2 進数に変換するには、10 進数の小数部分に「2」倍しつづけて、小数部分が 0 になるまで整数部分を取り出していきます。
取り出した整数部分を上から並べた「111」が、2 進数で表した「.875」になります
導き方さえ分かっていれば、10 進数の数を 2 進数に変換するのは意外と難しく無さそうですね!
コンピュータにおける 10 進数の計算方法
JavaScript に限らずコンピュータで 10 進数の計算をする場合は、
- 10 進数から 2 進数に一度変換してから計算
- 計算結果をまた 2 進数から 10 進数に戻す
- 結果を表示
というプロセスを基本的に経ています。
例えば 3 + 4 (7) は、コンピューターでは次のように計算されています。
シンプルに足し算しているように見えて裏側ではかなりややこしいことをしていることがわかりますね。
では、次に循環小数についてみていきましょう。
循環小数について
循環小数とは、小数点以下の数字が無限に続くうち、ある一定のパターンが繰り返される小数のことを指します。
例えば 0.1 と 0.2 をそれぞれ 2 進数に変換してみましょう。
10 進数の小数以下を 2 進数に変換する方法は先ほど説明していますので、みなさんも計算してみてくださいね。
0.1 の場合…
小数部分が 2, 4, 8, 6 でループを始め、0 になってはくれません。
つまり、10 進数の「0.1」は、2 進数では 0.0001100110011....
という循環小数になることが分かりました。
では、0.2 の場合はどうでしょうか。
0.2 の場合…
0.2 も同じく小数部分が 4, 8, 6, 4 でループを始め、0 にはなりません。
つまり、10 進数の「0.2」は、2 進数では 0011001100....
という循環小数になってしまい、数字は無限に続いていきます。
しかし、メモリ空間にも当然限りがあり、これらの無限に続く数をどこかで区切る必要があります。
この問題を解決する鍵が、浮動小数点数という技術になります。
ところで、なぜ「浮動」という文字が使われているのでしょうか?
この疑問に答える前に、対照的な概念である固定小数点数についてから考えてみましょう。
固定小数点数
10 進数の小数 1.875 を固定小数点数に変換してみます。
以下の例では 8 ビットで表現しており、整数部に 4 ビット、小数部に 4 ビットを割り当てています。
固定小数点数では、小数点の位置が事前に定められており、整数部と小数部をはっきりと区分けします。
この構造が非常にシンプルであるため、理解しやすいです。
しかし、ここで 1 つ大きな問題に直面します。
それは、数値を 2 進数へ変換した際に、その結果が割り当てられたビット数を超えてしまう場合です。
例えば...
10 進数の0.013671875 を 2 進数に変換すると、結果は0.000000111となります。
この場合、小数部に割り当てられたビット数が 8 ビットであれば、正確にこの値を表現できます。
しかし、今回の小数部に割り当てられているのは 7 ビットのため、8 ビット目以降がはみ出してしまいます。
原因はご覧の通り、固定小数点数では小数点の位置が予め固定されているため、表現できる数値の範囲と精度が限られてしまう点にあります。
そんな時に!!
浮動小数点数という技術が使われます!
浮動小数点数
浮動小数点数を使うことで、コンピュータは限られたビット数内で、非常に広い範囲の数値を表現することが可能になります。
では、浮動小数点数が何を行っているのか調べていきましょう!
基本構造
はじめに、浮動小数点数については IEEE(Institute of Electrical and Electronics Engineers)「米国電気電子技術者協会」が制定している IEEE754 というルールを基に解説をしていきます。(この記事では 64 ビットの倍精度浮動小数点数で解説を進めていきます。)
浮動小数点数は次のように符号・指数部・仮数部の形式で表現されます。
符号は数値の正負を示し、指数部は数値の大きさを調整します。
仮数部は数値の実際のデータを保持し、その精度を決定します。
それでは、先ほどの 10 進数の0.013671875 を 2 進数に変換した0.000000111を使って例を見てみましょう。
浮動小数点数で数を表現するには、まず数を「正規化」する必要があります。
正規化とは
正規化とは数を「1.xxxxx...」の形にすることです。
先ほどの 0.000000111 を正規化すると、1.11という形になり、ここで 2 のべき乗を使ってスケールを調整します。
この場合スケールは、2^-7 になります。
なお、下記例のように0.000000111×2^0と0.00111×2^-3それぞれ見た目は違えど、ともに同じ値を表しており、正規化すると1.11 × 2^-7になります。
では、正規化された数を浮動小数点数に変換してみましょう。
浮動小数点数への変換
2 進数の数値 0.000000111 を 64 ビットの IEEE 754 浮動小数点数に変換すると、以下のようになります。
符号部 : 正なので 0 。
指数部 : -7 にバイアスの 1023(後述)を加えた 1016。これを 2 進数に変換すると 01111111000。
仮数部 : 正規化された 1.11 から 1 を引いた 0.11 です。(1(一の位)は正規化した時点で必ず 1 になっていることが前提なので省略します。=>暗黙の 1 ビットと呼ぶ)これを 53 ビットで表現します。仮数部の 53 ビットに合わせるため、右側に 0 を足して調整します。
バイアスとは
IEEE 754 形式の 64 ビット浮動小数点数では、指数部に 1023 という数を足します。
これをバイアスと呼び、「下駄を履かせる」という表現もあります。
バイアスを使う理由は、指数部が負の数でも正の数として扱えるようにするためです。
例えば、指数が-1 の場合は、1023 を足して 1022 となります。
10 進数から IEEE754 における浮動小数点数への変換まとめ
0.013671875(10 進数)
→2 進数
0.000000111
→ 正規化
1.11 × 2^-7
→ 浮動小数点数(IEEE754)
符号部: 0
指数部: 01111111100
仮数部: 11011100000000000000000000000000000000000000000000000
をくっつけた
00111111110011011100000000000000000000000000000000000000000000000ができあがります。
これで 10 進数の数を浮動小数点数に変換する方法がわかりましたね!
0.1 + 0.2 が 0.3 にならないことを証明するまで後少しです!
一緒に頑張っていきましょう!
丸め処理
では 0.1 と 0.2 を浮動小数点数に変換してみましょう。
0.1(10 進数)
→2 進数
0.000110011001100110011001...
→ 正規化
1.10011001100110011001... × 2^-4
→ 浮動小数点数
符号部: 0
指数部: 01111111100(バイアス 1023 -4 = 1019 を 2 進数に変換)
仮数部: 100110011001100110011001...
をくっつけた
001111111100100110011001100110011001...ができあがります。
0.2(10 進数)
→2 進数
0.00110011001100110011001...
→ 正規化
1.10011001100110011001... × 2^-3
→ 浮動小数点数(IEEE754)
符号部: 0
指数部: 01111111101(バイアス 1023 -3 = 1020 を 2 進数に変換)
仮数部: 100110011001100110011001...
をくっつけた
001111111101100110011001100110011001...ができあがります。
0.1 と 0.2 の 2 進数はそれぞれ循環小数のため、決められた 64 ビットに収まりきりません。
IEEE754 では、このような超過した際には 64 ビットで表すための主なルールが定められています。
それは 2 つの表現可能な値のちょうど中間にある場合、偶数側の値に丸めるというものです。
10 進数の場合:
例えば、1.5 と 2.5 という数があったとします。
通常の四捨五入の場合、1.5 は 2 に2.5 は 3に丸められますが、IEEE 754 の偶数側の値への丸めを行うと、
1 と 2 の中間にある 1.5 は 2 に、
2 と 3 の中間にある 2.5 も 2 に丸められます。
2 進数の場合:
中間点より大きい場合:
2 進数で 1010...(10 進数での 10 より大きく 11 未満の数)の場合を考えます。
中間点は 1010.1(10 進数での 10.5)です。
例えば、1010.1101(10 進数での 10.8125)は中間点より大きいので、1011(10 進数での 11)に丸められます。
中間点より小さい場合:
例えば、1010.0110(10 進数での 10.375)は中間点より小さいので、1010(10 進数での 10)に丸められます。
中間点の場合:
1010.1(10 進数での 10.5)のように、1010(10 進数での 10)と 1011(10 進数での 11)の中間点にある場合を考えます。
この場合、最も近い偶数、つまり 1010(10 進数での 10)に丸められます。
私たちにとって身近な丸めの例は四捨五入ですが、浮動小数点数の丸めはこのような方法になっているのです。
では、この慣例にならって 0.1 と 0.2 の浮動小数点数も丸めを行なうと以下のようになります。
52 ビット目が 1 で、53 ビット目以降に少なくとも 1 つ以上の 1 が続くため、52 ビット目を繰り上げることになります。
その結果、仮数部を
仮数部: 1001100110011001100110011001100110011001100110011010
に確定します。
この結果により
0.1 の浮動小数点数は
符号部: 0
指数部: 01111111100(バイアス 1023 -4 = 1019 を 2 進数に変換)
仮数部: 1001100110011001100110011001100110011001100110011010
→0011111111001001100110011001100110011001100110011001100110011010
同じ方法で 0.2 に丸め処理を行うと
仮数部: 1001100110011001100110011001100110011001100110011010
になります。
0.2 の浮動小数点数は
符号部: 0
指数部: 01111111100(バイアス 1023 -3 = 1020 を 2 進数に変換)
仮数部: 1001100110011001100110011001100110011001100110011010
→0011111111011001100110011001100110011001100110011001100110011010
になりました。
では、最後にそれぞれの浮動小数点数を足しましょう!
浮動小数点数の足し算
浮動小数点数の足し算を行うときに、事前の準備が必要です!
それは指数部を同じにすることです。
指数部を合わせる際のルールとして、指数が大きい方に合わせるというルールがあります。
0.1 と 0.2 それぞれの指数は-4 と-3 ですね。そのため、-3 の指数を持つ 0.2 に合わせて 0.1 の指数を-3 にしたいと思います。
この際、整数部にあった暗黙の 1 ビットを右にシフトします。
足し算の結果、「10.0110011001100110011001100110011001100110011001100111」の数字が出ました。
整数部が 10.xxx になっているので、正規化を行うと右に 1 つシフトされて1.001100110011...になります。
右に 1 つシフトしたので指数部(01111111100)に 1 を足しましょう。
その結果...
符号部: 0
指数部: 01111111101
仮数部: 00110011001100110011001100110011001100110011001100111
となります。
この浮動小数点数を 10 進数に戻すと...
0.30000000000000004が現れます!!
記事の冒頭に出てきた数ですね!
私たちは普段 10 進数で計算しますが、コンピュータは一度 2 進数に変換してから計算し、計算結果を 10 進数に戻して表示をしてくれます。この際に、丸め誤差が発生することで 0.3 ではなく0.30000000000000004になってしまうんですね。
まとめ
コンピュータが計算する時はそれぞれの数を 2 進数に変換してから 10 進数に戻すという過程を経ており、これが原因で 0.1+0.2 が 0.3 にならないという仕組みでした。
みなさん、理解はできましたでしょうか?
実際に追ってみると複雑であるものの、納得していただけたのではないかと思います!
このような知識を持つことは普段私たちが機能を開発する際に、予期せぬバグを回避することに役立ちますので過程は理解せずとも、こういった問題が起きているということだけでも覚えていただければ幸いです。
こちらの記事は私自身にとって不慣れなテーマの元、四苦八苦しながら書き進めました。ところどころ、不適切な文言や数値が出ている可能性もあります。ご容赦ください...。
普段使う言語がどんな結果を出すのか調べてみましょう
以下のサイトでは、0.1+0.2 という式に対して各言語がどのような結果を出すのかをまとめて下さっています!
https://0.30000000000000004.com/
ぜひ、あなたが使用している言語の結果をチェックしましょう!
Discussion