2進数演算と2の補数ラボ

ビットの重みを可視化し、符号付き表現と列ごとの2進数演算を確認します。

インタラクティブシミュレーションを読み込んでいます...

加算器ひとつで引き算もできる 🖖

現代のコンピュータでは、負の整数は2の補数表現を使って表されます。最上位ビット(MSB)は負の重みとして機能し、8ビット整数ではビット7が+128ではなく-128を表します。減算が加算と同一になるという美しい性質があり、CPUはA - BをA + (~B + 1)として計算します。これにより別途の減算ハードウェアが不要となり、ALUが両演算に同じ加算回路を使用できます。

2進数は基数2の位取りにすぎない 🖖

普段の数では各桁が右隣の10倍の重みを持ちますが、2進数ではその倍率が単純に2になります。ビットは(右から)1, 2, 4, 8, 16, 32, … という重みを持ちます。2進数を読むとは、1が立っている桁の重みを足し合わせることです。1011 は 8 + 0 + 2 + 1 = 11 です。このツールのビット重み表示では各ビットを切り替えながら合計の変化を追えます。それがここでのすべての変換の種明かしです。

CPUはロシア農民のように掛け算する 🖖

ここで示す筆算の掛け算——Aを2倍し、Bに1のビットがある位置で足し合わせる——は、まさに「ロシア農民の掛け算」であり、3000年以上前のエジプトのパピルスにも見られる手法です。一方の数を半分にし(余りは捨てる)、もう一方を2倍にして、半分にした数が奇数の行だけ2倍した値を合計します。半分にして奇数かを調べることは、まさに2進数の桁を読むことなので、古代の書記官と現代のALUは同じアルゴリズムを実行しているのです。

8 ビット、四つの意味 — いま読んでいるのはどの符号化か

いまどの二進符号化にいるのか

1 バイトは、自分をどう読むべきかを何も教えてくれません。並び 11010110 は 214 でもあり、−42 でもあり、−41 でもあり、−86 でもあります。決めるのは事前に取り決めた約束だけで、ビット自身はどれなのかを告げられません。取り違えれば以降のすべてが誤りになり、しかも以降のすべてが正しく見えます。だから最初の問いは、答えがいくつかではなく、最上位ビットがいくらの重みを持つかです。下の四つの符号化はそれに四通りに答え、最後の二つの型は、そのうちの一つがなぜ回路を勝ち取ったかを示します。

符号なし — どの列も足すだけ w₇ = +128 → 0…255
2 の補数 — 最上位ビットは 128 の借り w₇ = −128 → −128…127
1 の補数 — すべてのビットを裏返して符号を変える w₇ = −127, 0 = ±0
符号と絶対値 — 数ではないビット w₇ = ±, 0 = ±0
減算器のない減算 a − b = a + (¬b + 1)
シフトと加算で掛ける a × b = ∑ (a ≪ i)

01

符号なし — どの列も足すだけ

わかっていること: 八つの重みはすべて 2 の正の冪で、1 から 128 まで。符号を表すものは何もないので、負になることもありません。範囲は 0 から 255 で、256 通りの並びがすべて使われます。

読み方: w₇ = +128 → 0…255

計算例: 85 + 11 → 01010101 + 00001011 = 01100000 = 96。桁上がりが下の列から上へ伝わります。さらに進めて 214 + 100 は 8 ビットでは 58 になります。本当の 314 から 256 を引いた値で、足りない 1 は桁上がり出力にぶら下がっています。

この型を開く: 符号なし加算
符号なし — どの列も足すだけ. 重みはすべて正。八つの列はただ足すだけで、0 から 255 まで。 八つの重みはすべて 2 の正の冪で、1 から 128 まで。符号を表すものは何もないので、負になることもありません。範囲は 0 から 255 で、256 通りの並びがすべて使われます。
重みはすべて正。八つの列はただ足すだけで、0 から 255 まで。

02

2 の補数 — 最上位ビットは 128 の借り

わかっていること: 正の重みが七つ、負の重みが一つ。ビット 7 は +128 ではなく −128 の値です。ほかは何も変わらず、範囲は −128 から 127 へ移ります。

読み方: w₇ = −128 → −128…127

計算例: 11010110 は −128 + 64 + 16 + 4 + 2 = −42 に分解されます。10 (00001010) を普通の列ごとの加算で足すと 11100000 = −128 + 64 + 32 = −32。まったく同じ 8 ビットが、符号なしでは 214 です。

この型を開く: 2 の補数
2 の補数 — 最上位ビットは 128 の借り. ビット 7 の重みは −128。だから 11010110 は −42 で、符号なしが 214 と呼ぶのと同じビットです。 正の重みが七つ、負の重みが一つ。ビット 7 は +128 ではなく −128 の値です。ほかは何も変わらず、範囲は −128 から 127 へ移ります。
ビット 7 の重みは −128。だから 11010110 は −42 で、符号なしが 214 と呼ぶのと同じビットです。

03

1 の補数 — すべてのビットを裏返して符号を変える

わかっていること: ビット 7 の重みは −127。負の数は絶対値をビットごとに裏返したものなので、−42 は 11010110 ではなく 11010101 になり、範囲は対称で −127 から 127 です。

読み方: w₇ = −127, 0 = ±0

計算例: −42 は 11010101 で、00101010 を裏返したものです。10 を足すと 11011111 = −127 + 64 + 16 + 8 + 4 + 2 + 1 = −32。これが正しいのは、最上位の列から何も出ていかなかったからにすぎません。代わりに −42 + 50 を試すと、素朴な和は 7 と読め、1 だけ足りません。8 に届かせるには、出ていった桁上がりを下からもう一度入れ直す必要があります。

この型を開く: 1 の補数
1 の補数 — すべてのビットを裏返して符号を変える. −42 は 42 を裏返しただけ。そして 11111111 は二つ目の、負の零です。 ビット 7 の重みは −127。負の数は絶対値をビットごとに裏返したものなので、−42 は 11010110 ではなく 11010101 になり、範囲は対称で −127 から 127 です。
−42 は 42 を裏返しただけ。そして 11111111 は二つ目の、負の零です。

04

符号と絶対値 — 数ではないビット

わかっていること: ビット 7 は重みをまったく持たない純粋な標識です。0 なら正、1 なら負。下の 7 ビットは 0 から 127 までの普通の絶対値を保ちます。

読み方: w₇ = ±, 0 = ±0

計算例: −42 は 10101010。符号ビットを立て、その後ろに 42 を 0101010 として置きます。人が数を書くやり方そのものですが、四つの符号化のなかで、両方の値を普通の加算器に渡すのが単に誤りである唯一の符号化です。10101010 + 00001010 は 10110100 となり、これは −32 ではなく −52 と読めます。

この型を開く: 符号と絶対値
符号と絶対値 — 数ではないビット. 符号ビットは重みを持たず、普通の加算器は −32 ではなく −52 を返します。 ビット 7 は重みをまったく持たない純粋な標識です。0 なら正、1 なら負。下の 7 ビットは 0 から 127 までの普通の絶対値を保ちます。
符号ビットは重みを持たず、普通の加算器は −32 ではなく −52 を返します。

05

減算器のない減算

わかっていること: 2 の補数で、演算は減算に設定。回路は減算用の仕掛けを持っていません。二つ目の値の符号を反転させて足すのです。

読み方: a − b = a + (¬b + 1)

計算例: 42 − 58 → 00111010 を裏返して 11000101、1 を足して 11000110、これが −58 です。ここに 00101010 を足すと 11110000 となり、−128 + 64 + 32 + 16 = −16 と読めます。

この型を開く: 加算で引く
減算器のない減算. 裏返し、1 を足し、それから足す。42 + (−58) は −16 に着地します。 2 の補数で、演算は減算に設定。回路は減算用の仕掛けを持っていません。二つ目の値の符号を反転させて足すのです。
裏返し、1 を足し、それから足す。42 + (−58) は −16 に着地します。

06

シフトと加算で掛ける

わかっていること: 符号なしで、演算は乗算に設定。二つ目の値の 1 のビットはどれも、そのビットの位置だけ左へずらした一つ目の値の写しを差し出します。

読み方: a × b = ∑ (a ≪ i)

計算例: 13 × 5 → 5 は 00000101 なので、ビット 0 と 2 が立っています。ずらさない 13 (00001101 = 13) と、二つ分ずらした 13 (00110100 = 52) が差し出され、13 + 52 = 65 = 01000001。

この型を開く: シフトして足す
シフトと加算で掛ける. 5 はビット 0 と 2 が立っているので、数に入る行は 13 と 52 だけです。 符号なしで、演算は乗算に設定。二つ目の値の 1 のビットはどれも、そのビットの位置だけ左へずらした一つ目の値の写しを差し出します。
5 はビット 0 と 2 が立っているので、数に入る行は 13 と 52 だけです。

全プロセスの詳細解説

  1. 8ビットに変換された42に対する2つの個別のオーバーフローフラグ 6 ステップ

    42を8ビットに2通りの方法で変換し、加算器を1つしか持たないCPUがなぜ2つの独立したオーバーフローフラグを持つのかを解明せよ。

    1. 位取り記数法は累乗の和であるため、最も直接的な方法はどの2の累乗が含まれているかを見つけることである。そのうち3つが含まれており、自ずとビットパターンが決まる。

    2. 機械的な手順を踏めば、探索することなく同じ答えが得られる。2で繰り返し割り、その余りを最下位ビットから順に並べる(得られた列を下から上へ読む)。

    3. 2の補数における符号反転は「ビット反転して1を加算」であり、その結果は 256 − 42 に等しい。仕組みの全貌はこれだけである。すなわち256を法とする算術であり、上位半分を負の数として再ラベル付けしたものである。

    4. 次にフラグについて述べる。キャリーアウトは最上位ビット位置の属性であるのに対し、オーバーフローは符号ビットへのキャリーインと符号ビットからのキャリーアウトの不一致である。

    5. 2つのフラグが一致しない数値の組み合わせを考える。バイトからのキャリーは発生しないため符号なし演算としては正しいが、符号ビットが反転しており、符号付き演算の結果は 256 だけ誤っている。

    6. 逆に、キャリーは発生するがオーバーフローはしない組み合わせで検証すれば、2つのフラグが存在する理由は明白となる。

    解答

    同じビット列が2つの異なる数値を意味し、どちらであるかを知っているのはプログラマだけだからである。 0110 0100 + 0011 0010 = 1001 0110 ではビット7からのキャリーが発生しないため C = 0 となり、符号なしとして解釈した 100 + 50 = 150 という結果は完全に正しい。同じ結果を2の補数として読むと −106 になり、これは無意味な値であるが、V = 1 がそれを示している。代わりに 200 + 100 を加算するとフラグの状態が入れ替わり、C = 1、V = 0 となる。加算器自身はそれを知る由もなく関知もしない。1つの和を計算して両方の警告を発するだけであり、その後コンパイラがどの命令を選択するかによって、どちらがバグを意味するかが決まる。これが、CおよびC++が符号付きオーバーフローを未定義動作とし、符号なしのラップアラウンドを定義している理由である。ハードウェアが両者を区別しており、言語仕様がそれをそのまま露出させることを選んだのである。

参考文献 (1)

例題

  • 符号なし加算 - 85 + 11 を二進で、桁上がりが列を伝わる様子つき。
  • 2 の補数 - 2 の補数:11010110 は −42 と読み、−42 + 10 = −32。
  • 1 の補数 - 1 の補数:最上位ビットは −127 の重みなので、−42 は 11010101。
  • 符号と絶対値 - 符号と絶対値:最上位ビットは純粋な符号なので、−42 は 10101010。
  • 加算で引く - 42 − 58 = −16。最上位ビットの負の重みによって、減算が加算になる仕組みを示します。
  • シフトして足す - 13 × 5 = 65 を、シフトと加算による二進の筆算で。