シリーズ「コンピュータの計算のしくみ」 第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 進法
-10.11010.8125
01.1011.625
111.013.25
2110.16.5
3110113

第 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 桁の浮動小数点 シミュレータ

マスを押すか、下のボタンで点を動かしてみてください

符号
指数
仮数
符号0プラス
指数01117 − 7 = 0
仮数0001.000

+1.000 × 201.000

1

仮数はそのままで、指数だけを変える

表せる数(席)の並び

浮動小数点(今回)固定小数点(前回・小数部 4 桁)016

赤い印が、いまの模様の大きさ。範囲を切り替えると、席の詰まり方の違いが見える

はじめは 0 0111 000 を入れてある。 指数は 7 − 7 = 0、仮数は 1.0001 である。

まず「点を右へ」を押してみてほしい。

2、4、8、16……と、押すたびに倍になる。 指数の 4 桁は 1 ずつ増えているが、仮数の 3 桁は 1 マスも動いていない。 変わっているのは、読み上げの行の赤い点の位置だけだ。

いくつか作ってみてほしい。

  • 0 1001 101 … 6.5。さっき組み立てたもの
  • 1 1001 101 … -6.5。符号の 1 桁を押しただけで、ほかは同じパターン
  • 0 1110 111240。この形式で表せる一番大きい数
  • 0 0001 0000.015625。先頭の 1 を省く読み方で表せる、一番小さい数

第 8 回の固定小数点では、同じ 8 桁で 0〜15.9375 だった。 240 にも、0.015625 にも届いている。

席は、やっぱり 256 個

では、タダで範囲が広がったのだろうか。

そうではない。パターンは 8 桁なので、席は第 8 回と同じ 256 個しかない。 変わったのは、席の並び方である。

シミュレータの下の「表せる数(席)の並び」を、0〜2、0〜16、0〜256 と切り替えてみてほしい。

固定小数点の席は、どこまでも等間隔に並んでいる。 浮動小数点の席は、0 の近くでぎっしり詰まり、離れるほどまばらになる。

指数表せる数隣との間隔
-60.015625 〜 0.0292968750.001953125
01 〜 1.8750.125
38 〜 151
7128 〜 24016

仮数は 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 === 0true になる。 前回、「等しいかどうかを調べる回路は、これを特別扱いしなければならない」と書いた。その特別扱いが、ちゃんと入っている。

足し算は、もうタダではない

次は計算である。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 と -1。差は 2
  2. 小さいほうの仮数を、差の分だけ右へずらす。 1.100 を 2 桁ずらして 0.011(これで指数が 1 にそろう)
  3. 仮数を足す
1.100
+0.011
1.111

1.111 × 21。8 桁で 0 1000 1113.75 である。合っている。

足した答えが 10.xxx のように 2 以上になったら、もう 1 手間かかる。 仮数を 1 桁右へずらし、指数を 1 増やして、1. で始まる形にそろえ直す

ずらすのは、第 6 回で見たとおりタダだ。 けれど「何桁ずらすか」を決めるには、指数の引き算が要る。 そして、どちらの指数が大きいかを比べなければならない。

引き算では、もっと比べる

3 − 2.5 を見てみる。

  • 3 は 1.100 × 21
  • 2.5 は 10.1 = 1.010 × 21

今度は指数がそろっているので、仮数をそのまま引ける。

1.100
1.010
0.010

0.010 × 21。先頭が 0 になってしまった。

そろえ直す。先頭に 1 が来るまで、仮数を左へ 2 桁ずらし、指数を 2 減らす。 1.000 × 2-10.5 である。

では、2.5 − 3 だったらどうか。 仮数を 1.0101.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 回の乗算器で掛ける。

1100
×1010
0000
1100
0000
1100
1111000

小数部 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 桁の別枠で持っているのは、こういうわけである。

指数に、下駄を履かせた理由

下駄をはく女性

背を7高くする

残っている謎が、もう 1 つある。 符号は別枠にしたのに、指数だけは「7 を引く」という読み方をしていた。

足し算の手順で、最初にやったのは比べることだった。 比べる回数は、少ないほどいい。

プラスの数をいくつか、8 桁のパターンで並べてみる。

パターン符号を除いた 7 桁を整数で読むと
0.50 0110 00048
0.750 0110 10052
10 0111 00056
30 1000 10068
6.50 1001 10177

数の大小と、パターンの大小が、そろっている。

指数が左、仮数が右に並んでいて、指数は小さいほどパターンも小さい。 だから、プラスどうしなら 7 桁をただの整数として比べるだけで、どちらが大きいか分かる。 第 7 回で見たとおり、比べるとは一度引いてみることだった。1 回の引き算で済む。

指数を 2 の補数にしていたら、こうはいかない。 -1 は 1111、1 は 0001。パターンの上では -1 のほうが大きく見えてしまう。

第 4 回の輪を思い出してほしい。 2 の補数は、輪を 01111000 のあいだで切った読み方だった。 下駄は、輪を 11110000 のあいだで切ったまま、名札を全部 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 を打つと Infinity0 / 0 を打つと NaN になる。止まらずに、答えの形で知らせてくる。

NaN はふしぎな数で、NaN === NaNfalse になる。 これも「等しいかどうかを調べる回路」の特別扱いである。

本物は、もっと長い

ここまでの 8 桁は、教材用に小さくした形である。 実際のコンピュータは、IEEE 754 という規格で決められた形を使っている。 1985 年に定められ、それまでメーカーごとにばらばらだった浮動小数点が、これで 1 つにそろった。

形式全体符号指数仮数下駄
この記事8 桁1437
単精度32 桁1823127
倍精度64 桁111521023

規則は、この記事で見たものとまったく同じだ。 符号は別枠の 1 桁。指数には下駄。仮数は先頭の 1 を省く。0 と ∞ と NaN には特別な模様。

JavaScript の数は、10.5 も、すべてこの 64 桁の倍精度で持たれている。 倍精度で表せる一番大きい数は、およそ 1.8 × 10308。 8 桁の 240 と比べると、ずいぶん遠くまで届く。

0.1 の席を、探してみる

第 8 回で、固定小数点では 0.1 を作れないことを見た。 0.0625 の席と 0.125 の席のあいだに、席が無かった。

点を動かせるようになった今なら、どうだろう。 シミュレータで、0.1 に一番近い数を探してみてほしい。

  • 0 0011 100 … 0.09375
  • 0 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で出すことができる。

次回、このシリーズの最終回で、その答えを見る。

参考文献