読む前にこの記事のポイントを音声で確認できます。この音声は本記事をもとにAIツール(NotebookLM)で自動生成したもので、発音や言い回しが不自然な箇所や、内容の誤りが含まれる可能性があります。理解の補助としてご利用ください。
前回までの記事で、コンピューターは半加算器と全加算器を組み合わせて足し算をしている、という話をしました。1 + 1 = 10(2 進数)も、3 + 1 = 4 の桁上がりも、全加算器で処理しています。
では、引き算はどうしているのでしょうか。AND や OR、NOT を組み合わせて、引き算専用の回路を別に作っているのでしょうか。実は、コンピューターは引き算をいったん足し算に置き換えてから計算しています。この記事では、その仕組みである「2 の補数」を、5 − 3 や 7 − 5 を例に説明します。
- 補数の考え方(お釣りと時計の例)と、1 の補数・2 の補数の作り方
- 2 の補数を使って、引き算を足し算の回路で計算する手順
- 2 の補数による負の数の表し方と、オーバーフローの注意点
そもそも、なぜ引き算は難しいのか
人間にとって 5 − 3 = 2 は簡単です。ところがコンピューターが扱えるのは 0 と 1 のスイッチだけで、ここまでに作れたのは半加算器と全加算器による足し算です。ここで引き算を作ろうとすると、次のような問題が出てきます。
| 問題 | 内容 |
|---|---|
| 借りる処理 | 10 進数で 52 − 8 をするときのように、上の桁から 1 を借りてくる処理が必要になる |
| 負の数の表し方 | 3 − 5 の答えは −2 だが、コンピューターの中にはマイナス記号がない |
| 回路の重複 | 足し算用と引き算用に、回路を2セット持つのは効率が悪い |
そこで、「足し算の回路はもうあるのだから、引き算もこれで済ませられないか」と考えます。その鍵になるのが「補数(ほすう)」という考え方です。
ステップ1:日常で体験している「補数」の感覚
お釣りの話
100円のジュースを買うのに、500円玉を出したとします。お釣りは 400円です。ここで、「500 から 100 を引く」という引き算は、「100 に 400 を足したら 500 になる」という足し算と同じ意味だと気づきます。
500 - 100 = 400 ↓ 言い換えると 100 + 400 = 500
引き算は、「足したら決まった数になる相手を探す問題」と言い換えられます。この「足すと基準の数になる相手」のことを、その基準に対する補数と呼びます。この例では、400 は 500 に対する 100 の補数です。
もう一つの例:時計
夜中の0時から3時間「前」は何時でしょうか。普通に計算すると 0 − 3 = −3 ですが、時計は12時間で一周するので、答えは時計の針で「9時」の位置(24時間表記なら21時)です。
3時間戻るのは、9時間進むのと同じ位置に行きます(3 + 9 = 12 で一周するから)。ここでも「引く」を「足す」に置き換えられています。9 は、12 で一周する世界での 3 の補数です。
ある決まった数で一周する世界では、引き算を足し算で表せます。コンピューターも、これと同じことをしています。
ステップ2:2 進数の世界の「補数」を作ってみる
では、2 進数で同じことをやってみます。補数は2種類あり、まず準備として 1 の補数、次にコンピューターが実際に使う 2 の補数を紹介します。
1 の補数 ― 全部ひっくり返すだけ
例として、4 ビット(4桁の2進数)の 0101(10 進数で 5)を考えます。1 の補数は、0 と 1 を全部ひっくり返すだけで作れます。
元の数: 0101 (=5) 1の補数: 1010
反転は NOT ゲート(反転回路)だけで作れるので、コンピューターにとって簡単な操作です。元の数と 1 の補数を足してみます。
0101 + 1010 ------ 1111
1111(10 進数で 15)になりました。これは 4 ビットで表せる最大の数(2⁴ − 1 = 15)です。「足すと 1111 になる相手」が 1 の補数です。
ただ、足して 1111 では、一周(10000)まで 1 だけ足りません。この 1 の差を埋めたものが、次の 2 の補数です。
2 の補数 ― 1 の補数に 1 を足すだけ
2 の補数は、1 の補数を作り、そこに 1 を足して作ります。0101(5)の 2 の補数を作ってみます。
ステップ1: 0101 → 1010 (0と1を反転) ステップ2: 1010 + 1 = 1011
5 の 2 の補数は 1011 です。元の数と 2 の補数を足してみます。
0101 (=5) + 1011 (5の2の補数) ------ 10000
答えは5桁の 10000 になりました。ここで、コンピューターが4ビットしか持っていないとします。5桁目への桁上がり(キャリー)は4ビットの外にはみ出すので、捨てられます。
10000 → (4ビットだから一番上を捨てる) → 0000
ゼロになりました。つまり4ビットの世界では、次の関係が成り立ちます。
5 + (5の2の補数) = 0
これは、次のように言っているのと同じです。2 の補数は、その数の「マイナス版」として振る舞います。
(5の2の補数) = -5
時計が12時間で一周したように、4ビットの世界も 0000 から 1111 まで進むと、16(10000)で一周して 0000 に戻ります。この一周する世界では、5 の 2 の補数が −5 の役割を果たします。
ステップ3:引き算を足し算でやってみる
ここまで来れば、答えはもうすぐです。A − B は、A に B の 2 の補数(つまり −B)を足せば計算できます。
5 − 3 を計算する
まずはタイトルの 5 − 3 です。3(0011)を反転すると 1100、1 を足すと 1101 で、これが −3 の役割をします。
| 計算 | 値 |
|---|---|
| 5 | 0101 |
| 3 の 2 の補数(−3) | 1101 |
| 足し算の結果 | 10010 |
| 5桁目のキャリーを捨てる | 0010(= 2) |
7 − 5 を計算する
もう1つ、7 − 5 も4ビットの 2 の補数で計算してみます。
| ステップ | やること | 結果 |
|---|---|---|
| 1 | 5 を 2 進数にする | 0101 |
| 2 | 5 の 2 の補数を作る(反転して 1 を足す) | 1011 |
| 3 | 7 と 5 の 2 の補数を足す | 0111 + 1011 |
| 4 | 5桁目へのキャリーは捨てる | 結果を読む |
0111 (=7) + 1011 (=5の2の補数、つまり -5 の役割) ------ 10010 ↑ この一番上の桁は4ビットからあふれるので捨てる → 0010 (=2)
答えは 0010(= 2)で、7 − 5 = 2 になりました。使った回路は前回までの全加算器と反転だけで、借りる処理も引き算専用の回路も出てきません。
「1 を足す」ために、加算器を2回使う必要はありません。加算器のいちばん下の桁には前の桁からのキャリー入力(C_in)があるので、引き算のときはここに 1 を入れます。さらに、B の各ビットを XOR ゲートに通し、引き算のときだけ反転させれば、1つの回路で足し算と引き算を切り替えられます(加減算器)。
- 足し算のとき:B をそのまま通し、C_in = 0 → A + B
- 引き算のとき:B を反転し、C_in = 1 → A + (B の反転) + 1 = A − B
引き算という一見別の操作が、「2 の補数を作る」という小さな前処理だけで、足し算に変わりました。これが、コンピューターが引き算専用の回路を別に持たなくてよい理由です。
ステップ4:2 の補数は負の数の表し方にもなっている
2 の補数は、そのまま「負の数」の表し方として使えます。4ビットで、各ビットの並びが何を意味するかを整理してみます。
| ビット | 符号なしとして読む | 2 の補数(符号付き)として読む |
|---|---|---|
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
| … | … | … |
0111 | 7 | 7 |
1000 | 8 | −8 |
1001 | 9 | −7 |
1010 | 10 | −6 |
| … | … | … |
1111 | 15 | −1 |
同じ 1011 というビットの並びが、「符号なし」として読めば 11、「2 の補数」として読めば −5 になります。ビットの並び自体は同じで、読み方が違うだけです。
2 の補数では、一番左のビットが 1 なら必ず負の数になります。これが「符号ビット」と呼ばれるものです。また、1 の補数では 0 が 0000 と 1111(−0)の2通りになってしまいますが、2 の補数では 0 は 0000 の1通りだけです。足し算の回路をそのまま使えることと合わせて、コンピューターが 2 の補数を使う理由になっています。
マイナス記号のような特別な記号を使わなくても、ビットの並びの読み方を決めるだけで、負の数を表せます。コンピューターは記号を持たないかわりに、「読み方の約束ごと」で扱える数の世界を広げています。
ステップ5:全体の流れを図で追ってみる
ここまでの流れを、A − B を計算する手順として図にまとめます。
どの処理も「反転」か「足し算」だけでできています。必要な部品は、反転のための NOT ゲート(実際の回路では、足し算と引き算を切り替えるための XOR ゲート)と、足し算のための全加算器だけです。新しい種類の部品は要りません。
ちょっとした補足 ― 完璧ではないところ
注意1:表せる範囲に限界がある(オーバーフロー)
4ビットの 2 の補数で表せるのは、−8 から +7 までです(n ビットなら −2ⁿ⁻¹ から 2ⁿ⁻¹ − 1)。たとえば 7 + 1 を計算すると、次のようになります。
0111 (=7) + 0001 (=1) ------ 1000 (=-8 !?)
正の数どうしを足したのに、結果がマイナスになりました。答えが表せる範囲を超えてしまうこの現象を、オーバーフローと呼びます。正の数どうしを足して負になったとき、または負の数どうしを足して正になったときがオーバーフローです。多くの CPU は、このときにオーバーフローフラグを立てて、プログラムが気づけるようにしています。
なお、さきほど 7 − 5 で捨てた5桁目へのキャリーは、オーバーフローではありません。2 の補数の計算では、4ビットを超えるキャリーはいつでも捨ててかまいません。
注意2:結果が負になる引き算では、キャリーが出ない
3 − 5 は −2 になるはずです。4ビットで計算すると、次のようになります。
0011 (=3) + 1011 (=5の2の補数 = -5) ------ 1110 (=-2、2の補数として読む)
今度は5桁目へのキャリーが出ません。結果の一番左のビットは 1 で、2 の補数として読むと −2 です。読み方さえ守れば、正しい答えになっています。
A と B がどちらも 0 以上の場合、A ≥ B(結果が 0 以上)ならキャリーが出て、A < B(結果が負)ならキャリーは出ません。どちらの場合も、キャリーは捨てて、残った4ビットを 2 の補数として読めば A − B になります。
まとめ
| 項目 | ポイント |
|---|---|
| 引き算の考え方 | コンピューターは引き算を直接せず、2 の補数を使って足し算に置き換える |
| 1 の補数 | ビットを全部反転するだけで作れる |
| 2 の補数 | 1 の補数に 1 を足すと作れる。その数の「マイナス版」として振る舞う(足すと 0 になる) |
| A − B の計算 | A + (B の 2 の補数) で計算できる。実際の回路では B を反転し、加算器の C_in に 1 を入れる |
| 負の数の表し方 | 2 の補数で負の数を表す。一番左のビットが符号ビットになり、0 は1通りだけ |
| 注意点 | 表せる範囲を超えるとオーバーフローになる。4ビットを超えるキャリーは常に捨てる |
これで、1つの加算回路で足し算と引き算の両方ができることが分かりました。前回までの記事と合わせて読むと、半加算器から引き算までが一本の線でつながります。
コメント