0.1+0.2≠0.3について

浮動小数点数を扱う時に注意しなければいけない例として、0.1 + 0.20.3にならない、というものがあります。 具体的には、0.1 + 0.20.30000000000000004になってしまい、0.3にならないプログラミング言語が多数存在します。 この問題について、以下の記事では十進数から二進数に変換するときの誤差が原因であるとしています(この説は他にも多くのウェブサイトで唱えられています)。

qiita.com

たしかに十進数から二進数に変換するときの誤差は一因なのですが、より大きな寄与を持っているのは足し算の丸め誤差です。 そのことを確認していきたいと思います。

(わかっている読者への注:以下では、浮動小数点数と言ったらIEEE754で規定されたbinary64(二進倍精度浮動小数点数)を指すこととします)

浮動小数点数の基礎

基本的には、分子が 2^{52}以上 2^{53}未満の整数で、分母が二の整数乗であれば、浮動小数点数として表せます。 この形式で表せない数は、浮動小数点数として表せる数で近似します。 この近似を行うことを、丸めと言います。 普通は、最も近い浮動小数点数で近似します(最近接丸め。端数が0.5ぴったりの時のタイブレークの方式の違いに四捨五入や偶数丸めがあります)。 特別な用途(例えばお金が虚空から生まれてはいけないとか*1)では、「真の値を超えない範囲で」最も近い浮動小数点数に丸める(下向き丸め)などを使うこともあります。逆に「真の値を下回らない範囲で」とする方法もあります(上向き丸め)。

ULPはunit of last placeの略で、上記のような分数として表したときの、分子の1の違いを単位としたものです。 誤差は0.38ULP、のような形で誤差の解析によく使います。

計算の過程

0.1を浮動小数点数に変換する

0.1に最も近い浮動小数点数は、 \frac{7205759403792794}{72057594037927936} = \frac{\lceil0.1\times2^{56}\rceil}{2^{56}}です(十六進浮動小数点数リテラル表記で0x1.999999999999ap-4)。 この時点で、分子が7205759403792793.6であるべきところが7205759403792794になってしまっているので、+0.4ULPの誤差を生じています。

0.2を浮動小数点数に変換する

0.2に最も近い浮動小数点数は、 \frac{7205759403792794}{36028797018963968} = \frac{\lceil0.2\times2^{55}\rceil}{2^{55}}です(十六進浮動小数点数リテラル表記で0x1.999999999999ap-3)。 この時点で、分子が7205759403792793.6であるべきところが7205759403792794になってしまっているので、+0.4ULPの誤差を生じています。

0.3を浮動小数点数に変換する

0.3に最も近い浮動小数点数は、 \frac{5404319552844595}{18014398509481984} = \frac{\lfloor0.3\times2^{54}\rfloor}{2^{54}}です(十六進浮動小数点数リテラル表記で0x1.3333333333333p-2)。 この時点で、分子が5404319552844595.2であるべきところが5404319552844595になってしまっているので、-0.2ULPの誤差を生じています。

0.1と0.2を足す

丸める前の加算の結果は、 \frac{5404319552844595.5}{36028797018963968}です。 ここで、分子は 2^{52}以上 2^{53}未満ですが、整数になっていないので丸めを行う必要があります。 四捨五入なり偶数丸めなりを適用すると、 \frac{5404319552844596}{36028797018963968} = \frac{\lceil0.3\times2^{54}\rceil}{2^{54}}を得ます。

これは、0.3に最も近い浮動小数点数である \frac{5404319552844595}{18014398509481984} = \frac{\lfloor0.3\times2^{54}\rfloor}{2^{54}}と分子が1だけ異なります。

誤差の解析

0.1の指数部は0.3の指数部に対して2だけ小さいので、変換の誤差+0.4ULPが最終結果に与える影響は1/4となって+0.1ULPです。 0.2の指数部は0.3の指数部に対して1だけ小さいので、変換の誤差+0.4ULPが最終結果に与える影響は1/2となって+0.2ULPです。 加算の丸め誤差が最終結果に与える影響は+0.5ULPです。

よって、合計で+0.8ULPの誤差が生じます。

このように、この誤差の主要因は加算の丸め誤差です。

いつでもfaithfulになるか

Faithful rounding(信頼可能丸め*2)とは、真の値を上向き丸めして得られる値か真の値を下向き丸めして得られる値の、どちらかが返ってくるような丸めのことです。 言い換えると、真の値を挟む二つの浮動小数点数のうちのどちらかが返ってくるということです。 「最近接の浮動小数点数に丸めてね」というのは要求が厳しすぎるので、少し要件を緩和したということです。

先ほどの例では、真の値0.3に対し、最も近い浮動小数点数 \frac{\lfloor0.3\times2^{54}\rfloor}{2^{54}}を計算結果として得ることができませんでした。 しかし、得られた結果 \frac{\lceil0.3\times2^{54}\rceil}{2^{54}}は真の値を上向き丸めして得られる値です。 したがって、先ほどの計算は、最近接丸めではありませんでしたが信頼可能丸めではあったようです。

さて、十進小数同士の足し算結果が必ずしも最近接丸めではないということがわかりましたが、要件を信頼可能丸めに緩めれば満たされるでしょうか?

答えは否です。 まず、桁落ちが生じる場合は明らかにかなりの誤差が生じ得ます。 桁落ちが生じない場合に限っても、信頼可能丸めですらなくなる例は存在します。

0.17 + 0.28がその一例です。

0.17を浮動小数点数に変換すると、 \frac{6124895493223875}{36028797018963968} = \frac{\lceil0.17\times2^{55}\rceil}{2^{55}}となり、+0.44ULPの誤差が生じます。 0.28を浮動小数点数に変換すると、 \frac{5044031582654956}{18014398509481984} = \frac{\lceil0.28\times2^{54}\rceil}{2^{54}}となり、+0.48ULPの誤差が生じます。 真の値である0.45を浮動小数点数に変換すると、 \frac{8106479329266893}{18014398509481984} = \frac{\lceil0.45\times2^{54}\rceil}{2^{54}}となり、+0.2ULPの誤差が生じます。

二つの浮動小数点数が表す値を加算すると、 \frac{8106479329266893.5}{18014398509481984}となり、四捨五入なり偶数丸めなりをすると \frac{8106479329266894}{18014398509481984}となります。 これは真の値 \frac{8106479329266892.8}{18014398509481984} =  \frac{0.45\times2^{54}}{2^{54}}を挟む二つの浮動小数点数ですらありません。 つまり、加算を実行するだけで、信頼可能丸めにすらならないケースがあることがわかりました。

このケースの誤差は+1.2ULPです。 内訳としては、0.17を変換した時の誤差+0.44ULPが最終結果に与える影響は1/2されて+0.22ULP、0.28を変換した時の誤差+0.48ULPが最終結果に与える影響は+0.48ULP、加算の丸め誤差が0.5ULP、で合計+1.2ULPです。


このような場合、最大の誤差は1.25ULPとなります。 まず、変換の誤差は最大で0.5ULPです。 加算のオペランドの変換誤差が最終結果に与える影響は、最大で0.5ULPですが、両オペランドともそうなることはありません。 なぜなら、そのような場合は繰上りが発生して、変換誤差が最終結果に与える影響が1/2になってしまうからです。 よって最悪ケースでは、片方のオペランドの変換誤差が最終誤差に与える影響が0.5ULP、もう片方のオペランドの変換誤差が最終結果に与える影響が0.25ULPとなります。 これに加えて加算の丸め誤差が最大で0.5ULPなので、合計で最終結果に乗る誤差は最大で1.25ULPであるということになります。

最大誤差が1.0ULP未満であれば信頼可能丸めになりますが、1.0ULPを超えているため、信頼可能丸めにはなりません。 ただし、最大誤差が1.5ULP未満なので、「真の値を最近接丸めした値か、またはその前後の浮動小数点数」が計算結果となることは保証されます*3

*1:言うまでもなく、お金を扱う時に浮動小数点数を使うべきではないですが、例として

*2:忠実丸め、隣接浮動小数点数丸め、F丸め、などの訳も存在

*3:指数部の境界付近ではULPを使った議論は怪しいです。反例があったら教えてください。