コンピュータの数値表現
コンピュータの数値表現(コンピュータのすうちひょうげん)では、コンピュータシステムにおける数の表現方法について解説する。
本項では固定長のビット列で表せる整数について、コンピュータの内部での表現に関してを主に扱う。
実数の近似表現などについては浮動小数点数の記事や任意精度演算の記事を、代数的数のコンピュータでの扱いなどといった話題については、適切な参考文献を参照のこと。
コンピュータにおいてはしばしば、有限の長さのビット列で数値を表す。例えば1ビットで2通りの値(0, 1)を表わせ、2ビットで2×2通り(00, 01, 10, 11)の値を表せる。ビット長で表現できる範囲において、整数の計算は誤差なく行えるが、一般の実数を表す際には丸め誤差が生じる。典型的には、数値は二進法で表されるため、2の冪以外での除算は循環小数として表さなければならない。有限の長さのビット列では循環小数の小数部をすべて表すことができず、有限小数で近似する必要がある。
例えば、
1 \times 3 \div 3
1 \div 3 \times 3
は有理数の範囲では等価だが、1 ÷ 3 を先に計算する場合、これは循環小数の近似値を与え、3 を掛けた結果は 1 に戻らない。結果を十進数で表す際、アルゴリズムによって 0.9999999999999999 や 0.99999999923475 のような値を表示する。
ある種の任意精度演算や数式処理システムでは、期待通り 1 や 0.999... という結果を与える。
コンピュータにおける数値表現は、以下に説明するビットを最小単位として、ビット列によってなされる。ビットは2通りの状態を持ち、ビット列は2の冪乗通りの状態を持つため、コンピュータ上の数値は非負のの二進整数に対応づけられる。
ビット(bit)は、1 または 0、on または off、yes または no など2つの状態をとり得るものとして解釈できる。物理的なアナロジーとしては、一つのトグルスイッチの状態を符号化したものがビットであるといえる。単一のビットは下表の2種類の状態のどちらかを必ず表す。コンピュータの論理回路である電子回路では、電位の高い (High) 低い (Low) を使うことも多いから、HとLという表現が使われることも多いこの対応は、逆でもよく、その場合負論理に相当する。電子回路では正逆を交互に使うと便利なこともある。
単一のビットでは2種類の値しか表せないが、2ビット、3ビットとビットを増やすことで倍々に組合せの数を増やせる(2ビットで4通り、3ビットで8通り)。
一般に b 個のビットが取れる状態の個数 N は N = 2b である。
バイト(byte)はコンピュータ上で1つの文字を表す単位で、その長さは文字コード表の大きさに依存するが、典型的には8ビットのビット列を指す。コンピュータ上で計算処理を行う際のデータ単位としてワードがある。1ワードに相当するビット列の長さはコンピュータ・アーキテクチャに依存して、バイトの倍数で表せる長さが選ばれる。1バイトが8ビットのコンピュータであれば、1ワードは従って8の倍数(特に2の冪)個のビットに相当する。
オクテット(octet)は8個のビットからなるデータ単位である。多くのコンピュータにおいて1バイトは8バイトであり、従ってバイトとオクテットは同義語となる。
しかしコンピュータネットワークの分野ではアーキテクチャに依存しないデータ単位が必要となるため、オクテットのような単位が主に使われ、(特定のアーキテクチャでは同義語であっても)バイトとは区別される。
ニブル(nibble)は4個のビットからなるデータ単位である。従って、オクテットの半分がニブルであり、1オクテットは2ニブルである。ニブルは16通りの状態を表わせる。0 から 15 の整数との対応例を表に示す。
コンピュータが使う論理はブール論理であり、これは2値の論理である。したがって二進法の2値とブール論理体系における2状態が直接対応可能である。
3値以上の値を識別するハードウェアは2値のハードウェアよりも複雑になる。
二進法は十進法よりもかなり効率がよい。初期のコンピュータは十進(二進化十進表現)を使っているものも多かった。しかし、電気回路のオンオフをそのまま利用できる二進法のほうが圧倒的に効率が良い。近年では、10ビット(二進法では 0〜1023 の1024通りが表現可能)を使って 0〜999 の1000通りを表現する、効率の良い Densely packed decimal といったものも考案されている。なお、初期のコンピュータ、特にリレー式の場合、入出力機器と比べ本体がたいして速くないため、入出力を十進でおこなうのであれば、二進←→十進の変換はそれなりに計算量が必要なので、むしろそのまま十進で扱ってしまったほうが効率が良いという場合もあった。なお「二進に比べて回路量が増え、信頼性が低くなるためである」といった説明が一般的だが、回路の冗長さを利用してエラーを検出する手法を併用し信頼性を上げているものがある。例えば、最初期のFACOMなどが挙げられる。一方で信頼性が低下する根拠としては、回路の持つインピーダンスによる電圧降下や電源や周辺回路によるノイズなどによって、本来の電圧が予定外の数値を表す電圧に変化してしまうことによる。
他の底を採用しようとした例もある。三進法を使ったコンピュータも、二進法よりも効率がよいのではないかと期待され、実験的に開発されたことがある。一般に数値を記号で表現するとき、三進法が最も効率がよいとされるが、二進法もそれとほぼ同程度の効率である。また、この効率に関する計算は、n状態の表現にはn個の素子が必要という仮定に問題があり、現実的には二進法を1素子の2状態で表現するのが圧倒的に有利である。三進法#経済性の記述も参照。
目的によっては、以上のような方法とは異なる表現法が便利なこともある。例えば、グレイコードなどがある。
八進法と十六進法は二進法の3桁と4桁に直接対応するので、コンピュータ関連の数を表現するためなどによく使われている。たとえば、整数値を二進法で 1001001101010001 などと表示しても、ぱっとその値を把握することはできない。なので、八進法や十六進法がよく使われる。
十進の体系では、10種類の数字(0 から 9)を組み合わせて数値を以下のように表す。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...
八進の場合、8種類の数字を使う(0 から 7)。
0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 ...
すなわち、八進の "10" は十進の "8" に等しく、八進の "20" は十進の "16" に等しい。
十六進では、16種類の数字を使う(0 から 9 の後に一般に A から F までが続く)。
0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 13 14 15 16 17 18 19 1A 1B...
すなわち、十六進の "10" は十進の "16" に等しく、十六進の "20" は十進の "32" に等しい。
これらはいずれも位取り記数法だが、十進法の桁の重み付けが10のべき乗であるのに対して、八進法では8のべき乗、十六進法では16のべき乗になっている。十六進や八進による表現から整数値を得るには、十進や二進などの他の記法の場合と全く同様に、各桁の数字にその桁位置の重み付け値をかけ、それらの総和を求めればよい。例えば、
八進 756
= (7 × 82) + (5 × 81) + (6 × 80)
= (7 × 64) + (5 × 8)  + (6 × 1)
=    448   +  40      +  6   = 十進 494
十六進 3b2
= (3 × 162) + (11 × 161) + (2 × 160)
= (3 × 256) + (11 × 16)  + (2 × 1)
=    768    + 176        +  2 = 十進 946
また、八進法の1桁は二進法の3桁にそのまま対応する。
000  =  八進 0
001  =  八進 1
010  =  八進 2
011  =  八進 3
100  =  八進 4
101  =  八進 5
110  =  八進 6
111  =  八進 7
同様に、十六進法の1桁は二進法の4桁にそのまま対応する。
0000  =  十六進 0       1000  =  十六進 8
0001  =  十六進 1       1001  =  十六進 9
0010  =  十六進 2       1010  =  十六進 a
0011  =  十六進 3       1011  =  十六進 b
0100  =  十六進 4       1100  =  十六進 c
0101  =  十六進 5       1101  =  十六進 d
0110  =  十六進 6       1110  =  十六進 e
0111  =  十六進 7       1111  =  十六進 f
そのため、二進による表現は、1001001101010001 のように長くても、単に数桁毎に区切って、そのまま書き換えるだけで、八進や十六進による表現に、書き換えできる。
001 001 001 101 010 001 二進 =
1   1   1   5   2   1          111521 八進
1001 0011 0101 0001 二進 =
9    3    5    1          9351 十六進
しかし、十進には、これらのような単純な書き換えで変換することはできず、整数値を経由して普通に変換することになる。
符号付きの数値の表現法はいくつか存在する。必ずしも「符号ビット」があるとは限らない。例えば、ゲタ履き表現では、ゲタの値によっては「符号ビット」としては扱えない。符号ビットがある場合は、最上位ビットが符号ビットのことが多い。なお、符号ビットについて「1なら負の数を表し、0なら正の数を表す」といったように考えるのが自然なのは「符号と絶対値」表現の場合だけであり、たとえば「2の補数」表現であれば、最上位ビットは −(2N) の重みを持つ桁である、と解するのが自然である。
「符号と絶対値」表現は、1ビットの符号ビットと、絶対値を表す残りのビット群からなる。例えば、4ビットの場合で、MSBの、0が正、1が負を示すものとすると、
0101 = +5
1101 = −5
となる。浮動小数点数はたいていがこのような形をしている。ただし、絶対値の部分が指数部と仮数部に分かれている。
1の補数とは、数のビット毎の反転が符号を反転させるとする表現である。これはビット単位のNOT演算に他ならない。例えば、
0101 = +5
1010 = −5
1の補数でも符号-仮数部表現でも、ゼロの表現が二種類存在するという問題がある。このため、どちらも最近のコンピュータでは滅多に使われない。1の補数では、
0000 = +0
1111 = −0
符号-仮数では、
0000 = +0
1000 = −0
2の補数は、ビット毎のNOT演算を施した後に1を加算することで得られる。例えば、
0101  =  +5
1011  =  −5
したがって、
0000  =  十進 0    1000  =  十進 −8
0001  =  十進 1    1001  =  十進 −7
0010  =  十進 2    1010  =  十進 −6
0011  =  十進 3    1011  =  十進 −5
0100  =  十進 4    1100  =  十進 −4
0101  =  十進 5    1101  =  十進 −3
0110  =  十進 6    1110  =  十進 −2
0111  =  十進 7    1111  =  十進 −1
この方式では整数であれば、16ビットで −32768 から 32767 の範囲、32ビットでは −2147483648 から 2147483647 の範囲を表現する。
2の補数の利点は、加減算かつオーバーフローを考えない範囲に結果が収まるような限りであれば、符号を気にせず符号無しの整数と同様に扱える点である。
例えば、5 + (−5) は以下のようになる。
+1011
ここで、数値は4ビットで表しているので、演算結果も4ビットでなければならず、したがって先頭の1は捨てられ、結果として期待した通りの0が得られる。
演算において符号を気にする必要がないのは、2n を法とした合同式になっているためである。例えば、15 ≡ −1 (mod 16) である。コンピュータは一般に固定のビット数で数値を表すので、法が一定であり理想的である。2の補数表現と符号なし表現の違いは、大小比較方法と表示方法である。
2の補数表現の唯一の奇妙な点として、表現可能な最も小さい数(16ビットなら −32768)の2の補数をとると自分自身になる点が上げられる。しかし、それが問題となることは滅多に無い。むしろ、符号無しの場合にオーバーフローになる演算が、2の補数表現ではごくあたりまえの、0をまたいだ計算(正確には、負の整数と非負整数にまたがった計算)であることのほうが、慣れるまでは非直感的かもしれない。
固定小数点数形式は、金銭勘定など浮動小数方式と相性が悪い場合、すなわちビジネスにおける計算(表計算ソフトやCOBOLなど)でよく使われる。「浮動小数点数の精度では十分ではない場合に採用される」というのは誤解である。単精度の場合に足りないようであれば、それは同じ長さの固定小数方式であっても足りてない。
整数部と小数部のビット数は、必要とされる精度や範囲に十分なように選ばれる。例えば、32ビット形式では、整数部に16ビット、小数部に16ビットといったように設定される。
桁位置の重み付けは、整数部と小数部で連続的となる。例えば整数部が、8の位、4の位、2の位、1の位となっている場合、小数部は0.5の位、0.25の位、0.125の位と続く。
例:
整数ビット群      小数ビット群
0.5    =   1/2  =  00000000 00000000.10000000 00000000
1.25   =   5/4  =  00000000 00000001.01000000 00000000
7.375  =  21/8  =  00000000 00000111.01100000 00000000
ただし、この形式では二進では表せない数が出てくる。例えば、1/5(十進では 0.2)は正確に表すことはできず、最も近い値は以下のようになる。
13107/65536  =  00000000 00000000.00110011 00110011  =  0.1999969... 十進の場合
13108/65536  =  00000000 00000000.00110011 00110100  =  0.2000122... 十進の場合
これは、桁を増やしても正確に表すことはできない。1/3 という数値を考えてみよう。これを十進の小数で表すと 0.333333... となって永遠に続く。これを適当な桁で止めると、その数値表現は 1/3 を正確に表すことはできていない。
つまり、十進で有限小数で表せる数が二進で有限小数になるとは限らない。これを回避する方法として、小数ではなく分子と分母を別々に格納した一種の分数として内部で保持する方式がある。しかし、平方根を求めるなどといった演算はできない。また、分数同士の加減算では通分によって分母が表現できないほど大きな値になる可能性があるため、「有理数型」というデータ型として、分母分子ともに多倍長整数で表すことが普通である。
絶対値が非常に大きな数や非常に小さな数を扱うには、浮動小数点方式を使う。
浮動小数点数は以下のような指数表記で数値を表現する形式である。
1.1030402E5 = 1.1030402
この方式の利点は、仮数の桁数だけでは表せない範囲の値を指数をかけることで表せる点にある。この方式の二進版がコンピュータ向けに定義されている。近年において最も一般的なものとして IEEE 754 があり、一例として以下のような「倍精度」の二進浮動小数点数形式(binary64)を定義している。
1ビットの符号部。符号を与える。
11ビットの指数部。「エクセス1023」形式。エクセス1023とは、指数を符号なしの整数 0 から 2047 で表し、実際の指数の値はそこから 1023 を減算したものとする方式である。
52ビットの仮数部。符号なしで、小数点以下の部分だけを保持し、小数点のすぐ上には常に "1" があるものとみなす(ケチ表現。ただし非正規化数の場合を除く)。
メモリ上では上位ビットから見て符号部、指数部、仮数部の順に並ぶ。
ここで s を符号部、e を指数部、b を指数のバイアス、f を仮数部とするとき、以下の値を表す。
(-1)^{s} \times 2^{e-b} \times 1.f
ただし、e = 0のときは非正規化数
(-1)^{s} \times 2^{-b+1} \times 0.f
となり、e = b * 2 + 1のときは ±∞ や NaN を表す。
binary64で、十進数で精度約15桁の数値を有効数字の以下の範囲の値を表現できる。
これ以外に特別な値として NaN(Not A Number)があるが、ここでは解説しない。
浮動小数点数も整数と同様、表せる値の範囲がある。また、精度も制限されている。binary64は十進で15桁程度の精度である。演算結果の桁数がそれより多い場合、誤差が生じる。例えば、非常に大きな数に非常に小さな数(ゼロに近い数)を加算すると、有効数字の桁の範囲が違いすぎるため元の大きな数が得られる場合がある。なお、拡大解釈して「浮動小数点数を使った演算では常に誤差が生じる」などというのは間違いである。たとえば、1.0 + 1.0 は厳密に 2.0 になる。
また、二進の浮動小数点表現の問題として、人類が多用する十進の小数表現でいわゆる「きりが良い」数との相性が悪い場合の存在がある。すなわち、0.75 のような十進でも二進でも有限小数で表せる数なら何の問題もない。しかし、例えば 0.1 という十進の小数は、二進では 0.000110011... というように無限小数になる。
低水準言語では、符号の有無や固定小数点か浮動小数点かを気にする必要がある。例えば浮動小数点の加算なのか整数の加算なのかによって、使用する命令が全く違ったものとなる。
LISPやPythonといった高水準言語では数値のデータ型はより抽象的であり、rational、bignum、complex などがある。それらの言語では、低水準言語と比べ、より抽象化され扱いやすいものとしてそれらの値が提供される。しかし「どんな数値であっても算術演算を正しく処理することが期待できる」というのは間違いで、コンピュータでは真の実数を扱うことはできないのだから、これらのデータ型でも同様に、それぞれの性質に注意して扱わねばならないことに違いは無い。演算子オーバーロードなどによって、符号無し整数、符号付整数、浮動小数点数、複素数など、複数のデータ型に対して、同じ見た目の式になるような言語もある。