シリーズ「コンピュータの計算のしくみ」 第11

回路が「丸める」とは?

0.30000000000000004

前回の最後に、JavaScript のコンソールで打ってもらったものがある。

0.1 + 0.2 === 0.3   // false

false である。では、0.1 + 0.2 は何になっているのか。

0.1 + 0.2           // 0.30000000000000004

0.3 より、ほんの少しだけ大きい。

電卓なら小学生でも間違えない計算を、世界中のコンピュータが、毎日こうして「間違えて」いる。 バグではない。どのブラウザで打っても、どの言語で打っても、同じ 4 が末尾に付く。

前回、宿題を 2 つ置いてきた。

  • 計算の答えが仮数の桁に収まらないとき、押し出された桁はどこへ行くのか
  • 席と席のあいだに落ちた数を、コンピュータはどこへ座らせるのか

この 2 つは、実は同じ問いである。そして、答えがそろったとき、末尾の 4 の正体が分かる。

答えは、いつもはみ出している

前回、1.5 × 2.5 を浮動小数点で計算した。

仮数の 3 桁と 3 桁を掛けたので、答えの小数部は 6 桁になった。1.111000。 8 桁の形式に戻すには、下の 3 桁 000 を捨てるしかなかった。 たまたま全部 0 だったので、何も失われなかった。

第 8 回の固定小数点でも同じことがあった。 小数部 4 桁どうしを掛けると小数部 8 桁になり、4 桁右へずらして戻した。 そのとき、下の 4 桁は押し出されて消えた。

掛け算だけではない。 足し算でも、点をそろえるために小さいほうの仮数を右へずらした。 ずらせば、右端から桁がこぼれる。 割り算にいたっては、0.1 のようにどこまで割っても終わらないことがある。

席の数が決まっている以上、計算の答えはいつもどこかではみ出している。 はみ出した桁を、どう始末するか。これを丸めという。

決め方は、1 つではない

十進法で、2.37 を小数第 1 位までにしたいとする。

  • 切り捨て … はみ出した 7 を、見ずに捨てる。2.3
  • 四捨五入 … はみ出した先頭が 5 以上なら、1 つ上げる。2.4

小学校で習ったのは四捨五入のほうだった。 2.3 と 2.4 のうち、2.37 に近いのは 2.4 だからだ。

2 進法でも、同じことができる。 ただし 2 進法の「半分」は、はみ出した先頭の桁が 1 かどうかだけで決まる。 十進法で 5〜9 と 0〜4 を見分けていたところが、1 と 0 の見分けで済む。

ところが、1 つだけ困る場面がある。ちょうど真ん中のときだ。

十進法の 2.35 は、2.3 と 2.4 のちょうど真ん中にある。どちらにも同じだけ近い。 四捨五入は、こういうとき必ず上へ上げると決めている。

もう 1 つ、別の決め方がある。

  • 偶数丸め … 近いほうへ寄せる。ちょうど真ん中のときだけ、一番下の桁が偶数になるほうへ寄せる

2.35 なら 2.4(4 は偶数)、2.45 も 2.4 になる。2.25 なら 2.2 だ。 2 進法でいう「偶数」は、一番下の桁が 0 のことである。

実際のコンピュータの浮動小数点が、何も指定しなければ使っているのは、この偶数丸めである。 なぜわざわざ、そんなまわりくどい決め方をするのか。それはあとで見る。

押して、確かめる

丸め シミュレータ

はみ出した桁を押して、決め方を切り替えてみてください

0.1 = 1.100 1100 1100… × 2⁻⁴。どこまでも続く

 1.
残る 3 桁
はみ出した桁

指数はそのまま(× 2-4

一番右の「…」は、その先にどこかで 1 が出てくるかどうか

切り上げるかどうかを決める回路

一番下 L = 0すぐ下 G = 1残り+…OR → S = 1ORAND切り上げ 1

すぐ下が 1 で、その先にも 1 がある(半分より大きい)ので、切り上げる

隣り合う 2 つの席と、本当の答え

真ん中0.093750.1015625

赤い点が本当の答え(その先の桁は、ごく小さく描いている)。青い席が丸めた行き先

1.100 + 0.0011.101 × 2-4

0.1015625

前回と同じ 8 桁の形式で、仮数 3 桁に収まらなかった答えを並べてある。 青い 3 桁が残る桁、紫の桁がはみ出した桁だ。一番右の「…」は、その先のどこかに 1 が出てくるかどうかを表している。

はじめは 0.1 を入れてある。 0.1 は 2 進法で 1.100 1100 1100… × 2-4 と、どこまでも続く数だった。

まず、決め方を切り替えてみてほしい。

  • 切り捨て … 0.09375 に落ちる
  • 四捨五入 … 0.1015625 に上がる
  • 偶数丸め … これも 0.1015625

数直線を見ると、赤い点(本当の 0.1)は、右の席にずっと近い。 近いほうへ寄せるなら、上がるのが正しい。切り捨ては、近さを見ていない。

次に「ちょうど真ん中(偶数)」を押してみてほしい。 1.000 のすぐ下に 1 があって、その先はすべて 0。1 と 1.125 の、ぴったり真ん中だ。

  • 四捨五入 … 1.125 に上がる
  • 偶数丸め … 1 のまま。一番下の桁が、もう 0(偶数)だから

3 つの決め方が食い違うのは、ほとんど真ん中のときだけである。 紫の桁をあちこち押して、行き先が変わる場所を探してみてほしい。

丸めも、回路になる

シミュレータの真ん中に、小さな回路が出ていた。 紫の桁は 4 桁と「…」があったのに、回路に入っているのは 3 本だけだ。

  • L … 残る桁の、一番下の桁
  • G … はみ出した桁の、先頭の 1 桁
  • S … はみ出した残りの桁を、全部 OR に通したもの

はみ出した桁が 4 桁でも 50 桁でも、丸めるのに知りたいことは 2 つしかない。

  • 半分以上か(G が 1 か)
  • 半分ちょうどか、それより大きいか(S がどこかで 1 になっているか)

S は、細かい桁を 1 つ残らず覚えている必要がない。 どこかに 1 があったかどうか、それだけを 1 桁に畳んでいる。 一度 1 が来たら二度と 0 に戻らないので、S はスティッキー(くっつく)ビットと呼ばれる。 右へずらしながら 1 桁ずつこぼれていく桁を、OR で次々に拾っていけばいい。

そして、3 つの決め方はそれぞれこう書ける。

決め方切り上げるのはゲート
切り捨て切り上げない何もつながない
四捨五入G が 1 のときG をそのまま
偶数丸めG が 1 で、L か S が 1 のときG AND (L OR S)

偶数丸めの式を読んでみる。

  • G が 0 … 半分未満。捨てる
  • G が 1、S が 1 … 半分より大きい。上げる
  • G が 1、S が 0 … ちょうど真ん中。L が 1(奇数)なら上げて偶数に、L が 0(偶数)ならそのまま

あのまわりくどい決め方が、ゲート 2 つに収まっている。

切り上げの 1 をどう足すかも、もう知っている。 第 2 回の全加算器を並べた加算器の、一番下の繰り上がり入力に、この 1 本をつなげばいい。 1.111 に 1 を足して 10.000 になったら、前回のように 1 桁ずらして指数を 1 増やす。

第 8 回で「押し出される 4 桁はどこへ行くのか」と書いた。 あのときの答えは、どこへも行かない。 4 つ隣に配線をつなぎ替えただけで、下の 4 本は何にもつながっていなかった。 何もつながないという回路が、切り捨てだったのである。

なぜ、偶数なのか

端数のある値札

端数は、どこかで必ず丸められている

四捨五入で困ることを、十進法で見てみる。

0.5、1.5、2.5、3.5 を、それぞれ整数に丸めてから足す。本当の合計は 8 だ。

四捨五入偶数丸め
0.510
1.522
2.532
3.544
合計108

四捨五入は、真ん中をいつも上へ運ぶ。1 回ごとの差は小さくても、足し続けると上に偏っていく。 偶数丸めは、真ん中を上と下へ半分ずつ振り分ける。偏りが打ち消し合う。

切り捨ては、もっと分かりやすく偏る。いつも下へ運ぶからだ。

実際に、それで数字が半分になった例がある。

1982 年、カナダのバンクーバー証券取引所は、新しい株価指数を 1000.000 から始めた。 指数は取引のたびに計算し直され、そのたびに小数第 3 位より下を切り捨てていた。 1 回に捨てられるのは 0.001 にも満たない。けれど、それを 1 日に何千回も繰り返した。

およそ 2 年後、指数は 524.811 まで下がっていた。 正しく計算し直すと、本当は 1098.892 だった。 市場は下がっていなかった。下がっていたのは、丸めだけだったのである。

8 桁で、0.1 + 0.2 をやってみる

いよいよ本題に入る。まず、前回の 8 桁の形式でやってみる。

入力の時点で、もう丸まっている。 0.1 と打ったつもりでも、コンピュータの中にあるのは、0.1 に一番近い席だ。

打った数2 進法偶数丸めで座る席
0.11.100 1100… × 2-41.101 × 2-4 = 0.1015625
0.21.100 1100… × 2-31.101 × 2-3 = 0.203125
0.31.001 1001… × 2-21.010 × 2-2 = 0.3125

0.1 も 0.2 も、少し上に座っている。0.3 も上だ。

丸めた0.1と0.2を足してみよう。前回の手順どおり、指数を比べて、小さいほうをずらして、足して、そろえ直す。 0.1015625 + 0.203125 = 0.3046875、2 進法で 1.001 11 × 2-2。 仮数 3 桁からはみ出た 11 を丸めると、G が 1、S が 1。上げて 1.010 × 2-20.3125

0.3 の席と、同じだ。8 桁では、0.1 + 0.2 は 0.3 になる。

拍子抜けしたかもしれない。では、0.3 + 0.4 はどうだろう。

  • 0.3 は 0.3125(1.010 × 2-2
  • 0.4 は 0.40625(1.101 × 2-2

指数がそろっているので、仮数をそのまま足す。

1.010
+1.101
10.111

10.111 × 2-2。そろえ直して 1.011 1 × 2-1、つまり 0.71875

はみ出したのは 1 だけで、その先は何も無い。ちょうど真ん中である。 L は 1(奇数)なので、偶数丸めは上げる。1.100 × 2-1 = 0.75

一方、0.7 と打ったときに座る席は 1.011 × 2-1 = 0.6875 だった。

0.75 と 0.6875。8 桁では、0.3 + 0.4 は 0.7 にならない。

シミュレータのプリセット「0.3 + 0.4 の和」と「0.7」で、2 つの行き先を見比べてほしい。

64 桁で、0.1 + 0.2 をやってみる

JavaScript の数は、前回見たとおり 64 桁の倍精度だった。やることは、8 桁のときとまったく同じである。

0.1 は、2 進法の仮数で 1001 が延々と繰り返す数だ。52 桁で打ち切ると、最後はこうなる。

打った数仮数 52 桁の最後の 8 桁座った席(10 進法で正確に書くと)
0.1…1001 10100.1000000000000000055511151231257827…
0.2…1001 10100.2000000000000000111022302462515654…
0.3…0011 00110.2999999999999999888977697537484345…

1001 と続くはずの最後が 1010 になっている。0.1 と 0.2 は、上に丸められている。 0.3 は、下に丸められている。

この 2 つを足すと、答えはどこに落ちるか。 0.3 のまわりの 2 つの席を見てみる。

  • 下の席 … 0.29999999999999998889…(0.3 が座った席)
  • 上の席 … 0.30000000000000004440…

上に丸まった 2 つを足した本当の和は、0.30000000000000001665…。 計算してみると、これは 2 つの席のぴったり真ん中に落ちている。

真ん中なら、偶数丸めの出番だ。 下の席の仮数は …0011(奇数)、上の席は …0100(偶数)。上へ行く。

0.1 + 0.2 は、0.3 の 1 つ上の席に座った。 それが 0.30000000000000004 の正体である。

では、64 桁で 0.3 + 0.4 はどうか。

0.3 + 0.4 === 0.7   // true

今度は、合う。

8 桁では 0.1 + 0.2 が合って、0.3 + 0.4 が合わなかった。 64 桁では、それが入れ替わった。

0.1 が特別に悪い数なのではない。 どの計算がぴったり合って、どれがずれるかは、席がどこに並んでいるか次第なのである。

ところで、ふしぎに思わなかっただろうか。 0.1 の席が 0.1000000000000000055… なら、なぜコンソールに 0.1 と打つと 0.1 と表示されるのか。

表示するときにも、丸めているからだ。 JavaScript は、「その席に戻ってこられる、一番短い 10 進法の書き方」を選んで見せている。 0.1 の席に戻れる一番短い書き方は 0.1 だ。 けれど 0.1 + 0.2 の席は 0.3 の席とは違うので、0.3 とは書けない。区別するために、17 桁目の 4 まで書く必要があった。

誤差は、積もる

1 回ずれるだけなら、0.00000000000000004 は小さい。 問題は、それを繰り返したときだ。

let x = 0
for (let i = 0; i < 10; i++) x += 0.1
x                   // 0.9999999999999999

0.1 を 10 回足しても、1 にならない。

1991 年の湾岸戦争で、アメリカ軍の迎撃ミサイル「パトリオット」のシステムは、時刻を 0.1 秒単位の整数で数えていた。 それを秒に直すために、24 桁の固定小数点で書いた 1/10 を掛けていた。

1/10 は、2 進法では終わらない。24 桁で切り捨てられた 1/10 は、本当の値より、およそ 0.000000095 だけ小さかった。

システムは 100 時間ほど止められずに動き続けていた。 0.1 秒ごとに積もった誤差は、時計を 約 0.34 秒ずらした。 高速で飛んでくるミサイルは、0.34 秒で数百メートル進む。 システムは飛来したミサイルを見失い、迎撃は行われなかった。兵舎に着弾し、28 人が亡くなった。

第 8 回で見た「0.1 は書けない」と、この記事で見た「切り捨て」。 どちらも回路としては、ごく小さな話である。

0 の近くの、特別な読み方

前回、「指数 0000 で仮数に 1 があるときは、最終回で少しだけ触れる」と書いた。約束を果たしておく。

前回の 8 桁で、先頭の 1 を省く読み方の一番小さい数は 0.015625 だった。 その下は、いきなり 0 である。

すると、こんなことが起きる。

  • x = 0.017578125(1.001 × 2-6
  • y = 0.015625(1.000 × 2-6

x と y は違う数だ。けれど x − y = 0.001953125 には、座る席が無い。 一番近いのは 0 である。x ≠ y なのに、x − y = 0 になってしまう。

「x と y が違うなら、x − y で割っても大丈夫」と考えて書いたプログラムは、ここで 0 で割ることになる。

そこで、指数 0000 のときだけ読み方を変える。 先頭の 1 を省かず、0.fff × 2-6 と読む。 こうすると 0 と 0.015625 のあいだに、0.001953125 刻みで 7 つの席が入る。 x − y は、そのうちの 1 つにちゃんと座れる。

0 に向かって、席の間隔がいきなり途切れずに、なだらかにつながる。 0 に潰れるのを、少しでも先に延ばすための席である。

丸めと、どう付き合うか

では、プログラムを書く人は、どうしているのか。

  • 「等しいか」ではなく「十分近いか」で比べる。 Math.abs(a - b) < 許せる幅 のように書く。JavaScript には、1 のまわりの席の間隔を表す Number.EPSILON が用意されている
  • お金は、整数で持つ。 第 8 回で見た「円ではなく銭や厘を単位にした整数」だ。整数なら、253 までは席のあいだに落ちない
  • 10 進法のまま持つ。 銀行の計算などでは、数を 10 進法の桁のまま持つ型を使う。0.1 はぴったり書ける

けれど、どの方法も席が有限であることからは逃げられない。 10 進法で持てば 0.1 は書けるが、1/3 は書けない。 整数で持てば 0.1 円は書けないし、どこかで割り算をすれば端数が出る。

どこを丸めるかを、自分で選べるようになるだけである。

席と席のあいだ

このシリーズでは、ずっと回路に何かをさせてきた。

足すこと。桁をつなぐこと。引くこと。 マイナスを扱い、あふれを見つけ、掛けて、割った。 小数を扱い、符号を決め、小数点を動かした。そして今回、丸めることまで回路に書けた。

どれも、0 と 1 のパターンと、そこに貼る名札の約束だった。 回路は何も理解していなかった。それでも、約束さえ決めれば、たいていのことは回路にできた。

けれど、1 つだけ、どうしてもできないことがある。

8 桁なら、席は 256 個。64 桁なら、およそ 1800 京個。 途方もない数だが、数え切れる。

一方、0 と 1 のあいだにある数は、数え切れない。 どんなに近い 2 つの席を選んでも、そのあいだには、また無限に数がある。 1/3 も、√2 も、円周率も、そのどこかにいる。

スイッチを何億個並べても、席は有限のままだ。 無限にある数を、有限の回路に座らせる。 丸めは、そのために必ず要る約束だった。

第 1 回で、こう書いた。

回路は足し算を理解していない。理解していないのに、答えは合っている。

最終回の今、こう書き足しておきたい。

回路は、一度も間違えていない。それでも、答えは合わないことがある。

電卓で 1 ÷ 3 × 3 を計算した画面

電卓も、席と席のあいだを持っていない

最後に、電卓を取り出してほしい。

多くの電卓は、数を 10 進法の桁のまま持っている。だから 0.1 + 0.2 は、ちゃんと 0.3 になる。 では、1 ÷ 3 × 3 を打ってみてほしい。

0.99999999 と出たなら、その電卓も、席と席のあいだに落ちた数をどこかへ座らせている。 1 と出たなら、どこかで誰かが、それをもう一度丸めている。

0.30000000000000004 の末尾の 4 は、有限の回路が、無限の数に触れた跡である。

次にどこかでその 4 を見かけたら、少しだけ思い出してほしい。 その下で、ゲート 2 つが、決められたとおりに正しく丸めていることを。

参考文献

シリーズ「コンピュータの計算のしくみ