Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 6 / 12·約 9 分·第25期 どの言語にもある仕組み

浮動小数点は1回ごとに正しく丸める — 0.1+0.2 のずれはどこで入るか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

アプリの中で数を扱う型が、計算1回ごとに何を約束し、何を約束していないかを見ます。

まず結論

0.1+0.2 が 0.30000000000000004 になるのは、足し算が雑だからではありません。0.1 と 0.2 は格納する時点で丸められており、足し算はその2つの正確な和を最も近い double に丸めています。今回は正確な和が上下の double のちょうど中間にあり、偶数丸めで上側が選ばれました。

1回ごとの結果は正しい。だからこそ、丸める回数と順序が変われば結果が変わります。何回・どこで丸めるかは、言語と型の約束しだいです。

? 読む前に予測

0.1 + 0.2 が 0.30000000000000004 になるとき、足し算という1回の演算の中では何が起きているでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

ずれは格納するときに入り、足し算は正しく丸める

「浮動小数点は不正確で、計算のたびに誤差が出る」とよく言われます。半分は当たっていますが、どこで何が起きているかを取り違えています。

まず 0.1 は double に入りません。Python の文書によれば、0.1 として格納されるのは 3602879701896397 / 2 ** 55 で、10進で書くと 0.1000000000000000055511151231257827021181583404541015625 です。最初の丸めは、書いた数を格納する時点で起きています。

では足し算は何をするのか。浮動小数点の規格 IEEE 754-2019 の本文は有料で、この教材では参照できていません。代わりに、1985年版の規格を解説した Goldberg の1991年の論文から引きます。2019年版でも同じ丸め方が生きていることは、後で引く ECMA-262 が IEEE 754-2019 の roundTiesToEven を参照している点で補います。

The IEEE standard requires that the result of addition, subtraction, multiplication and division be exactly rounded. That is, the result must be computed exactly and then rounded to the nearest floating-point number (using round to even).

ECMA-262 の Number::add も、手順の最後は Return 𝔽(ℝ(x) + ℝ(y)). です。2つの値を実数として正確に足し、その結果に最も近い Number を選ぶ。同じ距離なら仮数が偶数のほうを選び、仕様は This procedure corresponds exactly to the behaviour of the IEEE 754-2019 roundTiesToEven mode. と添えています。

0.1 + 0.2 で丸めが起きる場所
0.1 と書く10進の文字
0.2 と書く10進の文字
格納で丸める(1回ずつ)
格納値 0.13602879701896397 / 2^55
格納値 0.23602879701896397 / 2^54
正確に足す(ここでは丸めない)
正確な和10808639105689191 / 2^55
最も近い double へ丸める(1回)
0.30000000000000004上下が同距離なので偶数の側
  • ・丸めは3回。どれも「最も近い値」を選んでいて、手抜きの丸めは1回も無い
  • ・0.5 + 0.25 のように格納も和も丸めが要らなければ、結果は正確(0.75)
具体実物

0.1+0.2 の正確な和は、ちょうど中間にある

Python 3.14.7(aarch64 のコンテナ)で、fractions.Fraction を使って格納値どうしの正確な和を求め、周りの double と比べました。

Python 3.14.7 / aarch64 の出力(抜粋。3.11.16 でも同じ行)コマンド出力
exact sum (decimal, 80 digits) = 0.3000000000000000166533453693773481063544750213623046875
0.1+0.2 as float = 0.30000000000000004 = 0.3000000000000000444089209850062616169452667236328125 hex 0x1.3333333333334p-2
float(exact sum) == 0.1+0.2 ? True
exact - below = 1/36028797018963968 chosen - exact = 1/36028797018963968 above - exact = 3/36028797018963968
53-bit significands: below= 5404319552844595 (even? False) chosen= 5404319552844596 (even? True)

3行目が核心です。正確な和を float にすると、0.1+0.2 の結果と一致します。足し算は、与えられた2つの値に対して正しく丸めています。 36028797018963968 は 2^55 です。2^-55 を単位にして並べ直すと、位置関係がはっきりします。

2^-55 を単位にした位置(上の出力から python3 で計算)表
2^-55 単位の値 53ビットの仮数
下の double(= 0.3) 10808639105689190 5404319552844595(奇数)
正確な和 10808639105689191 (double で表せない)
上の double 10808639105689192 5404319552844596(偶数)

正確な和は上下のちょうど真ん中です。どちらを選んでも距離は同じなので、偶数丸めの規則で仮数が偶数の上側が選ばれました。下側はリテラル 0.3 の格納値なので、0.1+0.2 == 0.3 は False になります。

同じ 0.30000000000000004 は、Node v24.21.0 でも、gcc 13.5.0(-O0, aarch64)の C でも出ました。同じ結果です(gcc が C の Annex F に準拠しているかまでは確かめていません)。

✓ ここまでの確認

0.1 と 0.2 の格納値の正確な和は、隣り合う2つの double に対してどこにあり、なぜ上側が選ばれましたか。

抽象原理

1回ごとに正しいから、回数と順序で結果が変わる

1回ごとに正しく丸めるのなら、何回も足しても正しいのでしょうか。そうはなりません。丸めは演算ごとに独立に起き、前の丸めを次の演算は知りません。 ECMA-262 は Number::add に Finite-precision addition is commutative, but not always associative. と注記しています。入れ替えても同じだが、括弧の位置を変えると変わりうる、ということです。

Python 3.14.7 / aarch64(括弧と順序を変える)コマンド出力
(0.1+0.2)+0.3 = 0.6000000000000001 0.1+(0.2+0.3) = 0.6
1e16+1-1e16 = 0.0 1e16-1e16+1 = 1.0
sum([0.1]*10) = 1.0 loop = 0.9999999999999999 fsum = 1.0 exact(F) = 1.0

2行目は情報落ちです。1e16 に 1 を足すと、1 の寄与が丸めで消えます。先に 1e16 どうしを引けば 1 は残ります。

3行目は版に依存します。Python 3.14.7 の sum は 1.0、for ループで1つずつ足すと 0.9999999999999999。3.11.16 では sum も 0.9999999999999999 でした。 sum の文書には Changed in version 3.12: Summation of floats switched to an algorithm that gives higher accuracy and better commutativity on most builds. とあります(「most builds」なので、ビルドによる違いも残りえます)。float が変わったのではなく、足し方が変わりました。

もう一点、「1回ごとに丸める」は演算の単位についての約束で、式をどう演算に割るかは言語が決めます。Go spec は An implementation may combine multiple floating-point operations into a single fused operation と、複数の演算を融合して途中の丸めを省くことを許し(明示的な型変換で防げる)、C も contraction を許して実装定義としています(N3220 §6.5.1)。

足す順で結果が変わることが AI の出力の再現性にどう響くかは、第20期で扱います。

具体実物

丸めを最後の1回にする型と、10進で丸める型

Go 1.27.1 で、0.1+0.2 を定数で計算してから float64 に入れる場合と、変数どうしで足す場合を比べました。

main.go(main 関数の中身だけ抜粋)設定ファイル
const c = 0.1 + 0.2
var x float64 = c
fmt.Println(x == 0.3, x)
a, b := 0.1, 0.2
y := a + b
fmt.Println(y == 0.3, y)
$ go run main.go(Go 1.27.1 / aarch64)コマンド出力
true 0.3
false 0.30000000000000004

Go spec は Constant expressions are always evaluated exactly と定め、定数を型に入れるときは Rounding uses IEEE 754 round-to-even rules です。c は 0.3 として計算され、x に入れるときの1回だけ丸められるので、0.3 と書いたのと同じ double になります。Go の浮動小数点が正確なのではなく、丸める回数が1回に減っただけです。

なお 0.1+0.2 == 0.3 と比較まで定数で書くと、spec は A constant comparison always yields an untyped boolean constant. とし、float64 を経ずに比べるので丸めは0回です。1回の丸めを見るには、上のように一度型に入れる必要があります。

逆に、10進で丸める型もあります。Python の decimal の既定は prec=28, rounding=ROUND_HALF_EVEN です。

Python 3.14.7 / aarch64(decimal の既定のコンテキスト)コマンド出力
Decimal 1/3 = 0.3333333333333333333333333333 *3 = 0.9999999999999999999999999999 == 1 ? False
Decimal('0.1')+Decimal('0.2') == Decimal('0.3') ? True

Decimal("0.1") は格納で丸まらないので、0.1+0.2 は 0.3 に一致します。しかし 1/3 は 28 桁で丸められ、3 倍しても 1 に戻りません。10進型でも、結果が桁に収まらなければ丸めます。 基数と桁数が違うだけで、仕組みは同じです。

なお float の 1/3*3 は同じ環境で 1.0 に戻りました。丸めが偶然打ち消し合った結果で、float のほうが正確という意味ではありません。

✓ ここまでの確認

Go 1.27.1 で、`const c = 0.1 + 0.2` を float64 の変数 x に入れると `x == 0.3` は true、変数どうしの a+b では false でした。この差の説明として正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

Go の変数 a, b := 0.1, 0.2

0.1 と 0.2 を float64 に入れる時点で1回ずつ丸め、a+b でもう1回丸めます。正確な和がちょうど中間にあったため、0.3 の格納値ではなく上側が選ばれました。

Go 1.27.1(y == 0.3, y)コマンド出力
false 0.30000000000000004
具体例 B

Go の定数 c = 0.1+0.2 を x に入れる

spec は定数式を正確に評価すると定めます(実装は仮数 256 ビット以上でよく、表せなければ最も近い値に丸めてよい)。c は 0.3 になり、丸めは x に入れる1回だけで、0.3 と書いたものと同じ値になりました。

Go 1.27.1(x == 0.3, x)コマンド出力
true 0.3

同じ言語・同じ式で結果が分かれました。浮動小数点の「精度」が違ったのでしょうか。

それぞれで丸めが何回、どこで起きたかを数えてみてください。

この章のまとめ

  • 0.1 は 3602879701896397 / 2 ** 55 として格納される。ずれは格納の時点で入る。
  • 足し算は正確な和を最も近い double に丸める。0.1+0.2 の正確な和は上下のちょうど中間で、偶数丸めにより上側 0.30000000000000004 が選ばれた。
  • 丸めは演算ごとに独立なので、結合法則は成り立たないことがある。(0.1+0.2)+0.3 と 0.1+(0.2+0.3) は違う。
  • sum([0.1]*10) は Python 3.14.7 で 1.0、3.11.16 で 0.9999999999999999。sum の足し方が 3.12 で変わった。版に依存する。
  • Go の定数は正確に計算し、型に入れるとき1回丸める(比較まで定数なら丸めは0回)。Decimal は10進で丸めるが、1/3 は戻らない。違うのは丸める回数と基数で、「正確な型」ではない。
  • Go は演算の融合を許し、C も contraction を実装定義で許す。何回丸めるかは言語が決める。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Python の decimal(既定の精度 28 桁、ROUND_HALF_EVEN)で `Decimal(1)/Decimal(3)*3` を計算した結果について、正しいのはどれですか。

用語集(4語)
正しく丸める (exactly rounded)
結果を正確に計算してから、最も近い浮動小数点数に丸めること。Goldberg はこれを IEEE 規格が四則演算に要求する性質として説明する。
偶数丸め (round to even)
ちょうど中間のとき、最下位が偶数になる側を選ぶ丸め方。ECMA-262 は数値の選び方が IEEE 754-2019 の roundTiesToEven に一致すると書く。
情報落ち
大きさの違う数を足したとき、小さい方の寄与が丸めで失われること。近い数の引き算で有効桁が減る「桁落ち」とは別の現象。
融合演算 (fused operation / contraction)
複数の演算を1つの演算として扱い、途中の丸めを省くこと。Go spec は実装に許し、C は contraction として許して実装定義とする。