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

回路が「符号を決める」とは?

宿題が、2 つ残っている

実際のコンピュータは、あれほど便利だった 2 の補数を使わずに、符号を 1 桁の別枠で持っている。 前回、小数点を動かしたい、という話で終わった。 点の位置を 0 と 1 の中に書き込むと決めたのなら、符号をどこに置くかも決める必要がある。

その理由に手を伸ばす前に、片付けておかなければならない宿題がある。

  • 第 6 回。-8〜7 で読んだまま掛け算をすると、上の 4 桁が狂った。 下の 4 桁だけは合っていた
  • 第 7 回。マイナスの数の割り算で、余りをどう扱うかに触れずに済ませた

まずは第 6 回の宿題を片付けよう。

なぜ、上の桁だけが狂うのか

第 6 回の例を、もう一度見る。1111 × 0001、-1 × 1 である。

1111
×0001
00001111

回路の答えは 00001111。8 桁を -128〜127 で読むと、15 だ。 本当の答え -1 は、8 桁で書くと 11111111 である。

何が起きたのかは、回路の気持ちになれば分かる。 回路は 1111 を -1 だとは思っていない。15 だと思って掛けている。

15 は、-1 よりちょうど 16 大きい。 16 大きい数に 1 を掛けたので、答えも 16 大きくなった。 16 は 00010000ずれは、上の 4 桁に入る。

掛ける数の0001(1) が 3 なら、ずれは 16 × 3 = 48 になる。 ずれはいつも「16 の何倍か」なので、どれだけ育っても下の 4 桁には落ちてこない。 第 6 回で「下の 4 桁はいつでも正しい」と書いたのは、このためだった。

では、第 4 回で「16 違うことは、この回路にとって違わないのと同じだ」と言ったのは嘘だったのか。

嘘ではない。あれは 4 桁の世界の話だった。 4 桁の世界では、16 は 5 桁目に出て、消えてくれた。

掛け算の答えは 8 桁ある。 8 桁の世界で消えてくれるのは、256 のずれだけだ。 16 のずれは、しっかり残る。

直し方の方針は、これで決まる。 掛ける前に、2 つの数を 8 桁の世界の数とみなしてしまえばよい。

左を、符号で埋める

-1 を 8 桁で書くと、11111111 だった。 -3 なら 11111101。3 なら 00000011

4 桁のときと見比べてほしい。

4 桁8 桁
3001100000011
-1111111111111
-3110111111101
-8100011111000

右の 4 桁は、そのまま写してある。 左に足した 4 桁は、4 桁のときの一番左の桁を、そのまま繰り返しただけである。 プラスなら 0 で、マイナスなら 1 で埋める。

これで値が変わらないことは、第 4 回の読み方で確かめられる。 11111101 は -128 + 64 + 32 + 16 + 8 + 4 + 1 = -3。 左に 1 を 4 つ足した分と、-8 を -128 に替えた分が、ちょうど打ち消し合っている。

この操作を符号拡張という。 桁を増やすとき、左を 0 ではなく符号の桁で埋める

使う部品はない。4 桁目の線を、左の 4 本にも枝分かれさせてつなぐだけだ。 ここでも配線だけで済む。

では、掛け算の部分積を、みんな 8 桁に広げてから足してみる。 -3 × 3、1101 × 0011 だ。

11111101
×00000011
11111101
11111010
111110111

1 本目は -3 を 8 桁に広げたもの。2 本目はそれを 1 桁ずらしたもの(-6)。 足すと 9 桁目にあふれが出るが、8 桁の世界なので捨てる。

残るのは 11110111。-128 + 64 + 32 + 16 + 4 + 2 + 1 = -9。合っている。

広げた B は、1 行にまとめられる

掛けられる数、書ける数をそれぞれA,Bとおく。

では B がマイナスのときは、どうだろう。3 × -1、0011 × 1111 を考える。

方針は同じでいい。B も 8 桁に広げて、00000011 × 11111111 として掛ける。

00000011
×11111111
00000011
00000110
00001100
00011000
00110000
01100000
11000000
10000000
111111101

8 本目の部分積は、本当は 110000000 と 9 桁になるが、はみ出した 1 は 8 桁の世界なので捨ててある。 足した答えの 9 桁目も捨てると、残るのは 11111101-3 である。合っている。

A がマイナスでも B がマイナスでも、2 つとも符号で埋めてしまえば、第 6 回の乗算器のまま正しい答えが出る。 理屈としては、これで宿題は片付いた。

ただし、代償がある。部分積の本数を数えてほしい。 4 本だったものが、8 本になった。 AND も加算器も、ほぼ倍に増える。

しかも、増えた行はもったいない。 B の左に足した 4 桁は、B の一番左の桁を写しただけだった。 つまり 4 本目から 8 本目までの 5 本は、同じ 1 桁から生まれた行である。 B の一番左が 0 なら 5 本とも 0、1 なら 5 本とも A をずらした行になる。

その 5 本をまとめて足すと、どうなるか。

24 + 48 + 96 + 192 + 128 = 488。 8 桁の世界では 256 のずれは消えるので、488 − 256 = 232。11101000 だ。 これを -128〜127 で読むと、-24 である。

-24 は、A の -8 倍。 5 本の行は、合わせて「A の 8 倍を引く」1 本と同じだったのだ。

驚くことではない。符号拡張で確かめたとおり、100011111000 に広げても値は -8 のままだった。 左に足した 1 たちと 8 の位は、まとめて -8 の位の 1 枚の名札に集約できるのだ。

だから B は広げなくていい。4 本のまま、一番左の行だけを引けばいい。

0011
×1111
+00000011
+00000110
+00001100
00011000
11111101

3 + 6 + 12 − 24 = -3。さっきの 8 本と同じ答えが、4 本で出た。

引き算は、第 3 回で加算器にスイッチを 1 つ足すだけで済んだ。 だから最後の行だけ、スイッチを「引く」に倒しておけばいい。

なぜなら8の位ではなく-8の位とみなすことができるからだ。

まとめると、符号付きの掛け算に要るひと手間は 2 つだった。

  1. 部分積(ずらした A)の左を、符号で埋める
  2. B は広げる代わりに、一番左の行だけ足さずに引く

2 つとも、正体は「8 桁の世界に広げる」ことである。 A はそのまま広げ、B は広げた分を 1 行の引き算にたたんだ。 どちらも、第 4 回で貼り替えた -8 の名札を、掛け算にも思い出させているだけだ。

回路は -8 の位を知らない。だから掛け算のときは、こちらが教えてやる必要がある。

999 円の買い物

ここで、少し違う方向から掛け算を眺めてみる。

999 円の品物を買うとき、財布から 999 円ぴったりを数えて出す人は少ない。 千円札を 1 枚出して、1 円のおつりをもらう。 1000 を足して、1 を引く。 そのほうが手数が少ないからだ。

掛け算でも同じことができる。23 × 99 なら、こうだ。

23
×99
+2300
23
2277

99 は 100 − 1。だから 23 × 100 を足して、23 × 1 を引く。 9 × 3 も 9 × 2 も、九九は 1 回も出てこない。

2 進法では、この技がもっとよく効く。 2 進法で「9 がずらりと並ぶ」にあたるのは、1 がずらりと並ぶことだ。

0111 は 7。これは 10000001、つまり 8 − 1 である。 01111000 は 120。これは 1000000000001000、128 − 8 だ。

1 の並びは、並びのすぐ上の桁で 1 回足して、並びの一番下の桁で 1 回引けば作れる。 並びがどれだけ長くても、足し算と引き算の 2 回で済む。

3 × 7 を、第 6 回のやり方でやると、1 が 3 つあるので部分積を 3 本足す。 この技を使えば、2 本で済む。

0011
×0111
+00011000
00000011
00010101

24 − 3 = 21。合っている。

ブースの算法

この考え方を、回路に向いた手順にしたものをブースの算法という。 1951 年に、アンドリュー・ブースという人が発表した。

回路は「1 の並び」を一目で見つけることはできない。 けれど、隣り合う 2 桁を見るだけで、並びの端は分かる。

B を右から 1 桁ずつ見ていく。見るのは、その桁と、そのすぐ右の桁の組だ。 一番右の桁のさらに右には、0 があると思っておく。

その桁右の桁何が起きているかすること
101 の並びが、ここから始まるA をずらして引く
011 の並びが、すぐ右で終わったA をずらして足す
00並びの外何もしない
11並びの途中何もしない

0111 で確かめる。右から見ていく。

  • 1 の位:その桁 1、右は(あると思った)0。並びが始まる。A を引く
  • 2 の位:1 と 1。並びの途中。何もしない
  • 4 の位:1 と 1。何もしない
  • 8 の位:0 と 1。並びが終わった。A の 8 倍を足す

8A − A = 7A。さっきの 24 − 3 と同じ手順になった。

閉じない並び

ここからが、ブースの算法の本当にうまいところである。

B がマイナスのとき、たとえば 1111(-1)を同じ規則で見てみてほしい。

  • 1 の位:1 と 0。並びが始まる。A を引く
  • 2 の位、4 の位、8 の位:どれも 1 と 1。何もしない

並びは一番左まで続いて、閉じないまま終わる。 足す行は 1 本も出てこない。やったことは「A を 1 回引いた」だけだ。 つまり -A。B は -1 なのだから、合っている。

なぜだろう。

もし 5 桁目があれば、並びはそこで閉じて、A の 16 倍を足すはずだった。 けれど 4 桁の回路には 5 桁目が無いので、その足し算は起きない。 足しそこねた 16 倍のぶんだけ、答えは本来の 0〜15 の読みより 16A 小さくなる。

B を 15 ではなく -1 と読むこと。つまり、B から 16 を引いて読むこと。 「16A 小さい」とは、ちょうどそのことである。

さっき、符号付きの掛け算には「-8 の位の行は引く」というひと手間が要った。 ブースの算法では、そのひと手間をわざわざ書かなくていい。 閉じない並びが、勝手に -8 の位を引いてくれるからだ。

残るのは、部分積の左を符号で埋めることだけである。

押して、確かめる

3 つのやり方を、同じ入力で見比べられるようにした。 A も B も、-8〜7 で読む。黄色いマスが、符号で埋めた桁だ。

符号付きの乗算器 シミュレータ

A も B も -8〜7 で読みます。やり方を切り替えて、答えの 8 桁を見比べてください

A(-1)
B(1)×
B の 1 の位+00001111
B の 2 の位00000000
B の 4 の位00000000
B の 8 の位00000000
答え00001111
  1. 1 行目: 写して足す
  2. 2 行目: 0
  3. 3 行目: 0
  4. 4 行目: 0

1111 × 0001 = 00001111

-128〜127 で読むと 15。本当の答えは (-1) × 1 = -1

上の 4 桁が狂っている(下の 4 桁は合っている)

加算器を使った回数1
黄色のマス広げて埋めた桁

はじめは「そのまま(第 6 回)」で、1111 × 0001(-1 × 1)を入れてある。 答えの上の 4 桁が赤くなっているはずだ。

  • 「広げて、最後は引く」に切り替える。1 行目の左 4 マスが黄色の 1 で埋まり、答えが 11111111、-1 になる。
  • 1111 × 1111(-1 × -1)にする。「広げて、最後は引く」では、4 行目だけが「−」になる。答えは 00000001、1 だ。
  • 同じ 1111 × 1111 で「ブースの算法」を選ぶ。使うのは 1 行だけになる。-1 を 1 回引いて、1。
  • 0011 × 0111(3 × 7)。「広げて、最後は引く」では加算器を 3 回使うが、ブースの算法では 2 回で済む。

どのやり方でも、答えの下の 4 桁は変わらないことにも気づくと思う。 狂うのは、いつも上の 4 桁だけだった。

いつも得をするわけではない

ブースの算法は、1 の並びが長いほど得をする。 では、並びがまったく無いときはどうだろう。

シミュレータで 0011 × 0101(3 × 5)を、「広げて、最後は引く」と「ブースの算法」で比べてみてほしい。

0101 は、1 と 0 が交互に並んでいる。 並びが始まってはすぐ終わるので、ブースの算法ではすべての行で足すか引くかが起きる。 加算器の回数は 2 回から 4 回に増えてしまう。

999 円なら千円札を出すが、505 円を 1000 − 500 + 10 − 5 と払う人はいない。

そこで実際のコンピュータの乗算器は、B を 2 桁ずつ見るように工夫したブースの算法を使うことが多い。 そうすると、部分積の本数がほぼ半分になる。 中身は少し込み入るので、ここでは踏み込まないでおく。

大事なのは、次のことである。 2 の補数のまま掛け算をするには、どのやり方でも、-8 の位のことを回路に思い出させる手間が要る。

割り算は、どうするか

もう 1 つの宿題に移る。割り算だ。

第 7 回の割り算の回路は、引いてみて、一番左の桁を見て「引けたか」を判断していた。 その一番左の桁を、第 7 回では符号の桁と呼んだ。

ここで困ったことが起きる。 割られる数そのものがマイナスだと、一番左の桁は最初から 1 なのである。 「引けなかった」の印と、「もともとマイナスだった」の印が、同じ場所でぶつかってしまう。

たとえば 1001 ÷ 0010。-7 ÷ 2 のつもりでも、第 7 回の回路は 9 ÷ 2 だと思って割り、商 4、余り 1 を返す。

そこで、いちばん素朴な手を使う。

  1. 符号を横によけておく
  2. 2 つの数をどちらもプラスにして(絶対値にして)、第 7 回の回路でそのまま割る
  3. 最後に、よけておいた符号を付け直す

-7 ÷ 2 なら、7 ÷ 2 を割って 3 余り 1。ここまでは第 7 回と同じだ。 あとは符号を付けるだけ——なのだが、ここで意見が分かれる。

余りに、どちらの符号を付けるか

商の符号は、迷わない。 プラス ÷ マイナスはマイナス、マイナス ÷ マイナスはプラス。 2 つの符号が違えばマイナス、同じならプラス。

これは第 1 回の XOR そのものである。 符号の桁どうしを XOR に通せば、商の符号が出てくる。ゲート 1 つだ。

迷うのは、余りのほうである。 -7 ÷ 2 の答えとして、次の 2 つはどちらも筋が通っている。

余り確かめ算
-3-12 × (-3) + (-1) = -7
-412 × (-4) + 1 = -7

どちらも「割る数 × 商 + 余り = 割られる数」を満たしている。 余りの大きさも、どちらも割る数より小さい。 割り算の決まりだけからは、1 つに決まらないのである。

分かれ道で悩む画像

どちらに進んでも、間違いではない!

違いは、商をどちらへ丸めたかにある。-3.5 を、

  • 0 のほうへ丸めて -3 にすると、余りは割られる数と同じ符号(-1)になる
  • 小さいほうへ丸めて -4 にすると、余りは割る数と同じ符号(1)になる

絶対値で割ってから符号を付け直すやり方は、自然と前者になる。 7 ÷ 2 = 3 余り 1 に、両方ともマイナスを付けるだけだからだ。

そして、これは机上だけの話ではない。 プログラミング言語によって、選んだ側が違う。

  • C や Java、JavaScript では、-7 % 2-1
  • Python では、-7 % 21

同じ式が、言語によって違う答えを返す。どちらもバグではない。 「0 のほうへ丸める」と「小さいほうへ丸める」の、どちらの約束を選んだかの違いである。

JavaScript で「奇数かどうか」を n % 2 === 1 と書くと、マイナスの奇数では外れる。 -7 % 2 は -1 だからだ。よくある落とし穴である。

逆に「今日から 10 日前は何曜日か」のように、輪になった数を数えるときは、 余りがいつもプラスになる Python の約束のほうが素直に使える。

どちらが正しいかではない。何に使うかで、便利なほうが変わる。

-8 ÷ -1 は、どこへ行く

割り算には、もう 1 つだけ落とし穴がある。

-8〜7 の読み方で、1000 ÷ 1111、つまり -8 ÷ -1 をやらせてみる。 答えは 8 のはずだ。

けれど第 4 回・第 5 回で見たとおり、8 は -8〜7 のどこにも席が無い。 -8 だけは、プラス側に相方がいなかった。

割り算で答えがあふれるのは、実はこの組み合わせ 1 つだけである。 パソコンでよく使われる x86 の CPU は、これを 0 で割ったときと同じように例外として止める。 第 7 回で「0 除算は外から止めるしかない」と書いたが、止める理由がもう 1 つあったのだ。

符号を、別枠にしてみる

ここまでを振り返ると、1 つのことに気づく。

掛け算では、-8 の位を思い出させるために、符号で埋めたり最後の行を引いたりした。 割り算では、結局いったん符号を横によけて、絶対値で割って、XOR で付け直した。

割り算で使ったやり方を、はじめから数の表し方にしてしまったらどうだろう。

一番左の桁は、符号だけを表す。残りの桁は、大きさだけを表す。

4 桁なら、0011 は +3、1011 は -3 だ。 2 の補数の -3 は 1101 だったから、別の約束である。 この表し方を、符号と絶対値という。

氷点下の温度計

日常生活ではずっとこの書き方だった

この約束の掛け算は、驚くほど簡単になる。-3 × 3 なら、

  • 符号:1 と 0 を XOR に通して、1。マイナス
  • 大きさ:011 × 011 を、第 6 回の乗算器にそのまま入れて、9

符号で埋めることも、最後の行を引くことも、要らない。 割り算も同じで、大きさは第 7 回の回路のまま、符号は XOR 1 つだ。 余りの向きで迷うこともない。

では、足し算はどうだろう。 -3 + 5、1011 + 0101 を、第 2 回の加算器にそのまま入れてみる。

1011
+0101
10000

答えは 0000。+0 である。2 になってほしかったのに。

符号と絶対値の約束では、加算器にそのまま入れることができない。 符号が違う 2 つを足すときは、まずどちらの大きさが大きいか比べて、大きいほうから小さいほうを引き、大きいほうの符号を付ける。 第 7 回で見たように、比べるには一度引いてみるしかない。回路が「迷う」ことになる。

おまけに、0 が 2 つできてしまう。 0000 は +0 で、1000 は -0。 同じ 0 のはずなのに、模様が違う。等しいかどうかを調べる回路は、これを特別扱いしなければならない。

第 4 回の輪で、2 の補数は 0 を 1 つだけにして、全部の席を無駄なく使っていた。 あのきれいさは、ここには無い。

足し算の約束か、掛け算の約束か

2 つの約束を、並べてみる。

2 の補数符号と絶対値
足す・引く加算器にそのまま入れる大小を比べて、引く向きを選ぶ
掛ける符号で埋める・最後の行は引く大きさはそのまま、符号は XOR
割る絶対値にしてから割る大きさはそのまま、符号は XOR
0 のパターン1 つ2 つ(+0 と -0)
4 桁で書ける範囲-8〜7-7〜7

どちらが正しいという話ではない。 2 の補数は、足し算と引き算のための約束だった。 符号と絶対値は、掛け算と割り算に向いた約束である。

整数の世界では、足し算と引き算がとても多い。 配列の何番目かを数えるのも、繰り返しの回数を数えるのも足し算だ。 だから整数には、2 の補数が選ばれた。

では、点を動かす数ではどうだろう。

前回、足し算がタダだったのは、2 つの数の小数点がそろっていたからだった。 点の位置を 0 と 1 の中に書き込み、数ごとに違う場所へ動かせるようにしたら、 その「そろっている」は、まだ当てにできるだろうか。

そのとき、2 の補数のうまみは、どれだけ残っているだろうか。

ひとつだけ、手がかりを置いておく。 JavaScript のコンソールで Object.is(-0, 0) と打ってみてほしい。 JavaScriptのコンソールは今お使いのブラウザでWindows/Linuxなら Ctrl + Shift + J、Macなら Command + Option + Jで出すことができる。 これによってかっこ内のコンマの前後が同じかどうか調べることができる。

答えは false 、つまり違うのだ。

-0 は、実在する。

次回は、いよいよ小数点を動かす。

参考文献