シリーズ「コンピュータの計算のしくみ」 第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⁻⁴。どこまでも続く
指数はそのまま(× 2-4)
一番右の「…」は、その先にどこかで 1 が出てくるかどうか
切り上げるかどうかを決める回路
すぐ下が 1 で、その先にも 1 がある(半分より大きい)ので、切り上げる
隣り合う 2 つの席と、本当の答え
赤い点が本当の答え(その先の桁は、ごく小さく描いている)。青い席が丸めた行き先
1.100 + 0.001→1.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.5 | 1 | 0 |
| 1.5 | 2 | 2 |
| 2.5 | 3 | 2 |
| 3.5 | 4 | 4 |
| 合計 | 10 | 8 |
四捨五入は、真ん中をいつも上へ運ぶ。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.1 | 1.100 1100… × 2-4 | 1.101 × 2-4 = 0.1015625 |
| 0.2 | 1.100 1100… × 2-3 | 1.101 × 2-3 = 0.203125 |
| 0.3 | 1.001 1001… × 2-2 | 1.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-2、0.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)
指数がそろっているので、仮数をそのまま足す。
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 1010 | 0.1000000000000000055511151231257827… |
| 0.2 | …1001 1010 | 0.2000000000000000111022302462515654… |
| 0.3 | …0011 0011 | 0.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 回で、こう書いた。
回路は足し算を理解していない。理解していないのに、答えは合っている。
最終回の今、こう書き足しておきたい。
回路は、一度も間違えていない。それでも、答えは合わないことがある。
最後に、電卓を取り出してほしい。
多くの電卓は、数を 10 進法の桁のまま持っている。だから 0.1 + 0.2 は、ちゃんと 0.3 になる。 では、1 ÷ 3 × 3 を打ってみてほしい。
0.99999999 と出たなら、その電卓も、席と席のあいだに落ちた数をどこかへ座らせている。 1 と出たなら、どこかで誰かが、それをもう一度丸めている。
0.30000000000000004 の末尾の 4 は、有限の回路が、無限の数に触れた跡である。
次にどこかでその 4 を見かけたら、少しだけ思い出してほしい。 その下で、ゲート 2 つが、決められたとおりに正しく丸めていることを。
参考文献
- 松下俊介 著. 基礎からわかる論理回路. 第2版, 森北出版, 2021.7. 978-4-627-82842-1. https://ndlsearch.ndl.go.jp/books/R100000002-I031573740
- 馬場敬信 著. 算数で読み解くコンピュータのしくみ, 技術評論社, 2022.8. 978-4-297-12960-6. https://ndlsearch.ndl.go.jp/books/R100000002-I032268791
- IEEE Standard for Floating-Point Arithmetic. IEEE Std 754-2019, 2019.
- Goldberg, D. What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys, 23(1), 5–48, 1991.
- U.S. General Accounting Office. Patriot Missile Defense: Software Problem Led to System Failure at Dhahran, Saudi Arabia. GAO/IMTEC-92-26, 1992. https://www.gao.gov/products/imtec-92-26
- Huckle, T. "The Vancouver Stock Exchange". Technische Universität München. https://www5.in.tum.de/~huckle/Vancouv.pdf, (参照 2026-9-16).