シリーズ「コンピュータの計算のしくみ」 第10回
回路が「小数点を動かす」とは?
物差しを、持ち替えたい
第 8 回で、小数点は回路のどこにも無いことを見た。 8 桁の名札を全部 16 で割れば、同じパターンが小数になった。
けれど、点は固定だった。 目盛りを細かくすれば短い物差しになり、長くすれば目盛りが粗くなる。 その取引から逃げるには、点そのものを動かすしかない。
そして前回、その前に片付けておくことを片付けた。 2 の補数は足し算と引き算の約束で、符号と絶対値は掛け算と割り算の約束だった。
宿題は、2 つ残っている。
- 点の位置を、どうやって 0 と 1 の中に書き込むか
- 実際のコンピュータは、なぜ符号を 2 の補数ではなく 1 桁の別枠で持っているのか
1 つ目を片付けると、2 つ目の答えは向こうからやってくる。
とても大きい数は、どう書くか
理科の教科書で、こんな書き方を見たことがあると思う。
- 1 モルの粒の数 … 6.02 × 1023
- 電子 1 個の電気の量 … 1.60 × 10-19 クーロン
6.02 × 1023 を、ふつうに書くと 602000000000000000000000 である。 0 を 21 個も書くのは大変だし、数え間違える。
この書き方は、数を 3 つに分けている。
| 部分 | 6.02 × 1023 では |
|---|---|
| プラスかマイナスか | プラス |
| 数字の並び | 6.02 |
| 10 の何乗を掛けるか | 23 |
数字の並びは、いつも「1 の位に 0 でない数字が 1 つ、あとは小数」の形にそろえる。 602 × 1021 とも 0.602 × 1024 とも書けるが、6.02 × 1023 に決めておく。
では、10 の何乗の数を 1 増やすとどうなるか。 6.02 × 101 は 60.2、6.02 × 102 は 602。
数字の並びはそのままで、小数点だけが右へ 1 桁動く。
「10 の何乗」は、小数点がどこにあるかを書いた数だったのである。
2 進法でも、同じ書き方ができる
第 8 回で作った 6.5 は、2 進法で 110.1 だった。
これを「1 の位に 0 でない数字が 1 つ」の形にそろえる。
点を左へ 2 桁動かすと 1.101。動かした分は、2 の 2 乗を掛けて戻しておく。
110.1 = 1.101 × 22
十進法で 10 の何乗だったところが、2 の何乗になっただけだ。 2 の何乗を 1 増やせば、点は右へ 1 桁動く。1 減らせば、左へ 1 桁動く。
| 2 の何乗 | 点を動かすと | 10 進法 |
|---|---|---|
| -1 | 0.1101 | 0.8125 |
| 0 | 1.101 | 1.625 |
| 1 | 11.01 | 3.25 |
| 2 | 110.1 | 6.5 |
| 3 | 1101 | 13 |
第 8 回の表と見比べてほしい。あのときはパターンを固定して、点の位置を外から選んだ。 今度は点の位置を、数として書いておける。
この 3 つに、名前を付けておく。
- プラスかマイナスか … 符号
- 数字の並び(
1.101) … 仮数 - 2 の何乗か(2) … 指数
先頭の 1 は、書かなくていい
ここで、2 進法ならではのおまけがある。
十進法で「1 の位に 0 でない数字」と言えば、1 から 9 のどれかだった。 だから 6.02 の 6 は、ちゃんと書かないと分からない。
2 進法で、0 でない数字は 1 しかない。
0 以外の数をそろえた形にすると、仮数は必ず 1. で始まる。
必ず 1 なら、書く必要がない。書かなければ、1 桁ぶん得をする。
1.101 なら、しまっておくのは点の右の 101 だけでいい。
8 桁を、割り振る
第 8 回と同じ 8 桁で、この書き方をやってみる。 3 つの部分に、桁をこう割り振る。
| 符号 | 指数 | 仮数 |
|---|---|---|
| 1 桁 | 4 桁 | 3 桁 |
- 符号 … 0 ならプラス、1 ならマイナス。前回の「符号と絶対値」と同じ約束だ
- 仮数 … 先頭の
1.を省いた、点の右の 3 桁 - 指数 … 4 桁のパターンを 0〜15 と読んで、そこから 7 を引く
指数だけ、妙な読み方をしている。 指数はマイナスにもなりたいのに、第 4 回の 2 の補数ではなく「7 を引く」を選んでいる。 この 7 を下駄という。なぜ下駄なのかは、あとで分かる。
6.5 を入れてみよう。1.101 × 22 だった。
- 符号 … プラスなので
0 - 指数 … 2 に 7 を足して 9。4 桁で
1001 - 仮数 …
1.101の点の右、101
並べて 0 1001 101。これが 8 桁で書いた 6.5 である。
押して、確かめる
8 桁の浮動小数点 シミュレータ
マスを押すか、下のボタンで点を動かしてみてください
+1.000 × 20→1.000
1
仮数はそのままで、指数だけを変える
表せる数(席)の並び
赤い印が、いまの模様の大きさ。範囲を切り替えると、席の詰まり方の違いが見える
はじめは 0 0111 000 を入れてある。
指数は 7 − 7 = 0、仮数は 1.000。1 である。
まず「点を右へ」を押してみてほしい。
2、4、8、16……と、押すたびに倍になる。 指数の 4 桁は 1 ずつ増えているが、仮数の 3 桁は 1 マスも動いていない。 変わっているのは、読み上げの行の赤い点の位置だけだ。
いくつか作ってみてほしい。
0 1001 101… 6.5。さっき組み立てたもの1 1001 101… -6.5。符号の 1 桁を押しただけで、ほかは同じパターン0 1110 111… 240。この形式で表せる一番大きい数0 0001 000… 0.015625。先頭の 1 を省く読み方で表せる、一番小さい数
第 8 回の固定小数点では、同じ 8 桁で 0〜15.9375 だった。 240 にも、0.015625 にも届いている。
席は、やっぱり 256 個
では、タダで範囲が広がったのだろうか。
そうではない。パターンは 8 桁なので、席は第 8 回と同じ 256 個しかない。 変わったのは、席の並び方である。
シミュレータの下の「表せる数(席)の並び」を、0〜2、0〜16、0〜256 と切り替えてみてほしい。
固定小数点の席は、どこまでも等間隔に並んでいる。 浮動小数点の席は、0 の近くでぎっしり詰まり、離れるほどまばらになる。
| 指数 | 表せる数 | 隣との間隔 |
|---|---|---|
| -6 | 0.015625 〜 0.029296875 | 0.001953125 |
| 0 | 1 〜 1.875 | 0.125 |
| 3 | 8 〜 15 | 1 |
| 7 | 128 〜 240 | 16 |
仮数は 3 桁なので、どの指数でも席は 8 個ずつ並ぶ。 その 8 個の間隔が、指数を 1 増やすごとに倍になる。
隣との間隔は、その数の大きさのだいたい 1 割前後にそろっている。 0.02 のあたりでは 0.002 刻み、200 のあたりでは 16 刻み。
これは、ふだん私たちが数を扱う感覚に近い。 身長を測るならセンチの目盛りが欲しいが、東京から大阪までの距離を 1 センチ単位で知りたい人はいない。 大きい数は大ざっぱに、小さい数は細かく。 物差しの目盛りを、数の大きさに合わせて伸び縮みさせているのである。
範囲と細かさの取引から、逃げられたわけではない。 細かさを「どの数でも同じ幅」から「どの数でも同じ割合」に変えることで、取引の中身を変えたのである。
0 は、どこにいるのか
ここで、あることに気づくと思う。
仮数は必ず 1. で始まる、と約束した。
1.000 × 2何乗 を、どうやっても 0 にはならない。
先頭の 1 を省いたおまけの代わりに、0 を書く方法を失ったのである。
そこで、0 のためには特別なパターンを取り置いておく。
指数も仮数も全部 0 の 0 0000 000 を、0 と読む。
整数の 0 と同じパターンなので、覚えやすい。
それでは 0 0000 000 がもともと表していた数が、なくなってしまうのではないかと思うかもしれない。
ふつうに読んでみよう。仮数は 1.000。
指数は 0000 だが、下駄の 7 を引くので 0 乗ではなく -7 乗である。
1.000 × 2-7 = 0.0078125。
1 ではない。1 は 0 0111 000 だった。
0.0078125 は、この読み方で作れる一番小さい数、つまり 0 に一番近い席である。 席を 1 つ手放して 0 を手に入れるなら、0 のすぐ隣の席を手放すのが一番惜しくない。 下駄を履かせていたおかげで、全部 0 のパターンは、ちょうどその席に来ていたのである。
シミュレータで、指数を 0000 にしてみてほしい。
仮数が 000 なら 0 になる。仮数に 1 があるときは「0 の近くの特別な読み方」になるが、これは最終回で少しだけ触れる。
では、1 0000 000 はどうなるだろう。
符号が 1 で、大きさが 0。-0 である。
前回の最後に、JavaScript で Object.is(-0, 0) が false になることを見た。
あの -0 は、このパターンのことだった。
符号を別枠にしたのだから、0 にも符号が付いてしまう。前回の表の「0 のパターンが 2 つ」は、本物のコンピュータでもそのまま起きている。
実は -0 は、ただの持て余しものではない。
1 / -0 を JavaScript で打つと -Infinity になる。
マイナスの側からとても小さくなって 0 に潰れた数でも、どちらから来たかを覚えているのである。
一方で -0 === 0 は true になる。
前回、「等しいかどうかを調べる回路は、これを特別扱いしなければならない」と書いた。その特別扱いが、ちゃんと入っている。
足し算は、もうタダではない
次は計算である。3 + 0.75 をやってみよう。
- 3 は
11=1.100× 21。8 桁で0 1000 100 - 0.75 は
0.11=1.100× 2-1。8 桁で0 0110 100
仮数はどちらも 1.100 だ。では、仮数どうしを足せばいいのだろうか。
1.100 + 1.100 = 11.000。指数はどちらを使えばいいのか、分からない。
2 つの数の点が、そろっていない。
第 8 回で、固定小数点の足し算がタダだった理由は、2 つの数の点がそろっていたからだった。 点を動かせるようにした以上、数ごとに点の位置は違う。
十進法でも同じである。6.02 × 1023 + 1.5 × 1021 を、6.02 + 1.5 と計算する人はいない。 先に 1.5 × 1021 を 0.015 × 1023 に書き直して、点をそろえてから足す。
2 進法でも、同じ手順になる。
- 指数を比べる。 1 と -1。差は 2
- 小さいほうの仮数を、差の分だけ右へずらす。
1.100を 2 桁ずらして0.011(これで指数が 1 にそろう) - 仮数を足す
1.111 × 21。8 桁で 0 1000 111、3.75 である。合っている。
足した答えが 10.xxx のように 2 以上になったら、もう 1 手間かかる。
仮数を 1 桁右へずらし、指数を 1 増やして、1. で始まる形にそろえ直す。
ずらすのは、第 6 回で見たとおりタダだ。 けれど「何桁ずらすか」を決めるには、指数の引き算が要る。 そして、どちらの指数が大きいかを比べなければならない。
引き算では、もっと比べる
3 − 2.5 を見てみる。
- 3 は
1.100× 21 - 2.5 は
10.1=1.010× 21
今度は指数がそろっているので、仮数をそのまま引ける。
0.010 × 21。先頭が 0 になってしまった。
そろえ直す。先頭に 1 が来るまで、仮数を左へ 2 桁ずらし、指数を 2 減らす。
1.000 × 2-1。0.5 である。
では、2.5 − 3 だったらどうか。
仮数を 1.010 − 1.100 と引くと、答えはマイナスになって、第 4 回の輪を逆に回ってしまう。
だから、引く前に仮数の大きいほうを確かめて、大きいほうから小さいほうを引き、 大きかったほうの符号を付ける。
この手順に、見覚えがないだろうか。
答え合わせ
前回の表を、もう一度持ってくる。
| 2 の補数 | 符号と絶対値 | |
|---|---|---|
| 足す・引く | 加算器にそのまま入れる | 大小を比べて、引く向きを選ぶ |
| 掛ける | 符号で埋める・最後の行は引く | 大きさはそのまま、符号は XOR |
| 割る | 絶対値にしてから割る | 大きさはそのまま、符号は XOR |
符号と絶対値の弱点は、足し算の「大小を比べて、引く向きを選ぶ」だった。 2 の補数の強みは、足し算の「加算器にそのまま入れる」だった。
ところが、点を動かす数では、足し算はもう加算器にそのまま入れられない。 符号をどちらの約束にしても、指数を比べて、ずらして、そろえ直す手間から逃げられない。
- 指数を比べるついでに、どちらの数が大きいかはほとんど分かっている
- 大きいほうから小さいほうを引くのは、比べたあとなら難しくない
2 の補数を選んで守りたかった足し算のうまみは、点を動かすと決めた時点で、もう消えていた。
では、掛け算はどうか。1.5 × 2.5 をやってみる。 第 8 回で、固定小数点の点の数え方を確かめたのと同じ例だ。
- 1.5 は
1.100× 20 - 2.5 は
1.010× 21
十進法で (1.5 × 100) × (2.5 × 101) を計算するとき、 数字の並びどうしを掛けて、10 の何乗は足す。2 進法でも同じだ。
仮数どうしを、第 6 回の乗算器で掛ける。
小数部 3 桁と 3 桁を掛けたので、答えの小数部は 6 桁。1.111000 だ。
指数は 0 + 1 = 1。
1.111 × 21 = 3.75。
- 仮数 … 第 6 回の乗算器にそのまま入れる
- 指数 … 第 2 回の加算器で足す
- 符号 … 第 1 回の XOR に通す
符号で埋める必要も、最後の行を引く必要もない。 そろえ直しも、仮数どうしの積は 1 以上 4 未満なので、ずらしても 1 桁だけで済む。
割り算も裏返しで、仮数を第 7 回の除算器で割り、指数を引き、符号は XOR だ。
足し算はどちらの約束でも面倒で、掛け算と割り算は符号が別枠のほうが素直に通る。 それなら、符号は別枠にしたほうが得になる。
おまけもある。
- 符号を反転するのは、1 桁を反転するだけ。2 の補数のように全部反転して 1 を足さなくていい
- 範囲がプラスとマイナスでぴったり対称になる。前回の -8 ÷ -1 のような、相方のいない数が出てこない
実際のコンピュータの浮動小数点が、符号を 1 桁の別枠で持っているのは、こういうわけである。
指数に、下駄を履かせた理由
残っている謎が、もう 1 つある。 符号は別枠にしたのに、指数だけは「7 を引く」という読み方をしていた。
足し算の手順で、最初にやったのは比べることだった。 比べる回数は、少ないほどいい。
プラスの数をいくつか、8 桁のパターンで並べてみる。
| 数 | パターン | 符号を除いた 7 桁を整数で読むと |
|---|---|---|
| 0.5 | 0 0110 000 | 48 |
| 0.75 | 0 0110 100 | 52 |
| 1 | 0 0111 000 | 56 |
| 3 | 0 1000 100 | 68 |
| 6.5 | 0 1001 101 | 77 |
数の大小と、パターンの大小が、そろっている。
指数が左、仮数が右に並んでいて、指数は小さいほどパターンも小さい。 だから、プラスどうしなら 7 桁をただの整数として比べるだけで、どちらが大きいか分かる。 第 7 回で見たとおり、比べるとは一度引いてみることだった。1 回の引き算で済む。
指数を 2 の補数にしていたら、こうはいかない。
-1 は 1111、1 は 0001。パターンの上では -1 のほうが大きく見えてしまう。
第 4 回の輪を思い出してほしい。
2 の補数は、輪を 0111 と 1000 のあいだで切った読み方だった。
下駄は、輪を 1111 と 0000 のあいだで切ったまま、名札を全部 7 だけずらした読み方である。
切れ目がパターンの端にあるから、大小の順がくずれない。
0 を 0 0000 000 にしたのも、ここで効いてくる。
一番小さい指数のパターンの、一番小さい仮数。0 は、パターンの上でもちゃんと一番下にいる。
指数が 1111 のとき
最後に、反対側の端を見ておく。
シミュレータで、指数を 1111 にしてみてほしい。
- 仮数が
000なら、∞(無限大) - 仮数にどこか 1 があれば、NaN(Not a Number、数ではない)
0 1110 111 の 240 に、同じくらいの数を足すと、席が足りなくなる。
だから答えは ∞ としている。
第 5 回を思い出してほしい。 整数の 7 + 1 は、回路が黙ったまま -8 になった。 輪を一周してしまったからだ。
浮動小数点では、あふれた数はマイナスに化けない。 端に取り置いた ∞ の席に、止まる。
0 で割ったときも同じだ。
第 7 回と前回で、整数の 0 除算や -8 ÷ -1 は、外から止めるしかないと書いた。
JavaScript で 1 / 0 を打つと Infinity、0 / 0 を打つと NaN になる。止まらずに、答えの形で知らせてくる。
NaN はふしぎな数で、NaN === NaN は false になる。
これも「等しいかどうかを調べる回路」の特別扱いである。
本物は、もっと長い
ここまでの 8 桁は、教材用に小さくした形である。 実際のコンピュータは、IEEE 754 という規格で決められた形を使っている。 1985 年に定められ、それまでメーカーごとにばらばらだった浮動小数点が、これで 1 つにそろった。
| 形式 | 全体 | 符号 | 指数 | 仮数 | 下駄 |
|---|---|---|---|---|---|
| この記事 | 8 桁 | 1 | 4 | 3 | 7 |
| 単精度 | 32 桁 | 1 | 8 | 23 | 127 |
| 倍精度 | 64 桁 | 1 | 11 | 52 | 1023 |
規則は、この記事で見たものとまったく同じだ。 符号は別枠の 1 桁。指数には下駄。仮数は先頭の 1 を省く。0 と ∞ と NaN には特別な模様。
JavaScript の数は、1 も 0.5 も、すべてこの 64 桁の倍精度で持たれている。
倍精度で表せる一番大きい数は、およそ 1.8 × 10308。
8 桁の 240 と比べると、ずいぶん遠くまで届く。
0.1 の席を、探してみる
第 8 回で、固定小数点では 0.1 を作れないことを見た。 0.0625 の席と 0.125 の席のあいだに、席が無かった。
点を動かせるようになった今なら、どうだろう。 シミュレータで、0.1 に一番近い数を探してみてほしい。
0 0011 100… 0.093750 0011 101… 0.1015625
やはり、無い。
0.1 は 2 進法で 0.000110011001100… と、どこまでも終わらない数だった。
点をどこへ動かしても、仮数の 3 桁に収まらない並びは、収まらない。
64 桁の倍精度にすれば、席はずっと細かくなる。 それでも仮数は 52 桁で終わるので、0.1 の席は、ぴったりには無い。
もう 1 つ、置いてきたものがある。
1.5 × 2.5 で、仮数の積は 1.111000 と小数部 6 桁になった。
8 桁の形式に戻すとき、下の 3 桁は押し出されている。今回はたまたま全部 0 だった。
0 でなかったら、押し出された桁はどこへ行くのだろう。
席と席のあいだに落ちた数を、コンピュータはどこへ座らせるのか。
JavaScript のコンソールで、0.1 + 0.2 === 0.3 と打ってみてほしい。
JavaScriptのコンソールは今お使いのブラウザでWindows/Linuxなら Ctrl + Shift + J、Macなら Command + Option + Jで出すことができる。
次回、このシリーズの最終回で、その答えを見る。
参考文献
- 松下俊介 著. 基礎からわかる論理回路. 第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.