はじめに
?進数
皆さんは何進数で計算していますか?
こう問われると、一瞬考え込んでしまうかもしれませんが、通常は10進数を用いていると思います。
10進数というのは、各桁を0~9で表し、それを超えるような時は10という様に桁上がりする記法です。
さて、コンピューターは計算が得意ですが、どのように行っているのでしょうか。
大多数のコンピューターにおいて、計算は2進数で行われています。
これは、コンピューターが電気のオン、オフを2進数の1、0として計算していることに起因します。
この1と0の単位をビットと言いますが、このままだとわかりづらいので、8ビットをまとめて1オクテットや1バイトと称します。
8ビットを使うのであれば、0000 0000~1111 1111の256通り、16ビット使えば65536通りの表現が可能です。
気になる計算の仕方ですが、2進数でも考え方は10進数と変わりません。
違いは、1+1が2ではなくて、10になることです。これは、2進数は0~1までしか使えず、2を超えると桁上がりするためです。
以下は4ビット同士の加算です。
0010 +)0011 ----- 0101
難しく考えることはありません。
0+0=0、1+0=1、0+1=1、1+1=10を覚えれば計算できます。
符号付数値表現
数値を表現するのに、符号が必要です。
符号は、数値が0より大きいか小さいかを表すために付加されます。
0より大きい数値は自然数とも言い、通常は数字をそのまま記述するか「+」を付加して表します。
0より小さい負の数を表現するには「-」(マイナス記号)を付けて表します。
例えば-100という様に記述すれば、マイナス100だということがすぐわかります。
では、2進数ではどうすればよいでしょうか。
前述のとおりコンピューターは基本的に1、0で処理することしかできません。
符号+仮数部
最初に思いつくのが、適当なビットを符号として使うことです。
試しに最上位ビットを符号と見立てて4ビットの範囲で表を作成してみました。
符号は0が正、1が負です。
符号 数値 10進数 0|000 | 0(+0) 0|001 | 1 0|010 | 2 0|011 | 3 0|100 | 4 0|101 | 5 0|110 | 6 0|111 | 7 1|000 | 0(-0) 1|001 | -1 1|010 | -2 1|011 | -3 1|100 | -4 1|101 | -5 1|110 | -6 1|111 | -7
これで-7~7まで表現することが出来ました。
0の表現が2種類ありますが、取り合えず必要な数は表現できていますね。
この表現は初期のコンピューターで採用されていたことがあります。
この状態で、先ほどの4ビットの加算を行ってみましょう。
0010 (2) +)0011 (3) ----- 0101 (5)
括弧の数字は10進数に変換した値です。正しく計算出来ていますね。
では、負の数をそのまま加算してみましょう。
0010 (2) +)1011 (-3) ----- 1101 (-5)
正しい答えは-1ですが、そうなりませんでした。この表現だと負の数を加算する時は、
負の数であることを事前にチェックして、絶対値の大きい方から絶対値の小さい方を減算した上で符号の処理が必要です。
0010は正の数で絶対値は010 1011は負の数で絶対値は011 絶対値の大きい011から010を減算して、001 絶対値の大きい数は負の数だったので、負の記号を表すビットを付加して、答えは1001(-1)
人間の計算の仕方と同じですが、もっと効率が良い方法はないでしょうか?
1の補数表現
2進数で負の数を表現するのに1の補数表現があります。
これは、正の数の1と0を反転させた状態のビット列を、その数の負の数とします。
以下はその対応表です。符号の列を設けていますが、符号+仮数部表現と意味が少し違います。
4ビットで表せる範囲は16種類です。半分の0~7を正の数に割り当てた場合、その反転状態における数値の最上位ビットは必ず1になるので、
ここを確認すれば、符号が分かるということです。
符号 数値 10進数 0|000 | 0(+0) 0|001 | 1 0|010 | 2 0|011 | 3 0|100 | 4 0|101 | 5 0|110 | 6 0|111 | 7 1|111 | 0(-0) 1|110 | -1 1|101 | -2 1|100 | -3 1|011 | -4 1|010 | -5 1|001 | -6 1|000 | -7
この形式で、うまく加算が出来るか試してみましょう。
0010 (2) +)1100 (-3) ----- 1110 (-1)
0011 (3) +)1100 (-3) ----- 1111 (-0)
うまく計算出来ています。しかし、次の例はどうでしょう?
0010 (2) +)1110 (-1) ----- 0000 (0) 桁溢れ発生!
計算結果が4ビットに収まらない状態、つまり桁溢れが発生したせいで正しい答えが求められませんでした。
別のパターン
0111 (7) +)1100 (-3) ----- 0011 (3)
どうも桁溢れが発生するとうまく計算できないようです。
ここで、桁溢れが発生した場合の結果をよく見てください。
2+(-1)=0(本当は1)
7+(-3)=3(本当は4)
どうやら桁溢れが発生した時に、1だけ結果にずれが生じているようです。
ということは、桁溢れが発生した時は、自動的に+1すれば正しい結果が得られそうです。
結論から言うと1の補数表現では計算結果の桁上がり状況を(キャリーと言います)、
結果に加算することで正しい結果を得られます。
かなり単純に計算できるようになりました。実際、1の補数表現は今でも使われる場所はありますし、
古いコンピューターでは一般的でした。
しかし、さらに改良された表現形式があります。
2の補数表現
1の補数では、キャリーの加算が必要でした。更に0の表現が2種類あります。
これを解決した2の補数表現という方式があります。
難しいことは何もなく、1の補数に1を加算することで得られます。
以下はそのパターン表です。
0の表現が1種類になりました。その分範囲が増えたので、-8~7まで表現できます。
符号 数値 10進数 0|000 | 0 0|001 | 1 0|010 | 2 0|011 | 3 0|100 | 4 0|101 | 5 0|110 | 6 0|111 | 7 1|111 | -1 1|110 | -2 1|101 | -3 1|100 | -4 1|011 | -5 1|010 | -6 1|001 | -7 1|000 | -8
先ほどうまくいかなかったパターンを試してみましょう。
0010 (2) +)1111 (-1) ------ 0001 (1)
桁溢れは発生していますが、正しい結果が得られています。
0111 (7) +)1101 (-3) ------ 0100 (4)
これもうまく計算出来ています。
どうでしょうか、桁溢れが起きてもうまく計算できるというか、桁溢れをうまく利用しているように見えます。
コンピューターは加算しかできない
このように書くと、誤解されるかもしれません。
減算命令が存在しないと言っているのではありません。大抵のCPUの命令セットを確認すると、減算命令は存在します。
ただし、減算命令を実行する場合、内部的に2の補数表現に変換してから加算命令で減算を実現する場合が殆どです。
アーキテクチャーに絡む話ですが、減算を実現するロジックを作成して実装するのに比べて、
補数と加算を用いて実装した方が効率が良いからです。
おわりに
2進数の基礎的なことについて少しだけ書いてみました。
2の補数なんて必要なの?という方の疑問に少しでも答えられたのであれば幸いです。
実務ではJavaの様な高級言語を使っている限り、そんなに難しいことは覚えなくていいのかもしれません。
しかし、オーバーフローを原因とするバグはこれらの理解がないと発見すらできないかもしれません。
覚えておいて損はないので、覚えておくことをお勧めいたします。
- 当ページの人物画像はNIGAOE MAKERで作成しました。