コンピュータを構成している基本的なデジタル回路は電圧の高低によって数を表現します。そのためコンピュータはその構成上「0」か「1」、この二値いずれかを用いて数を表現しています。
2進数(正の整数)
まず、2進数の前に一番身近な10進数での数の表現方法を考えてみます。10進数は「0123456789」の10種のいずれかを使用して数を表現します。例えば123の場合、10進数だと当然ですが「123(10)」となります。右下の(10)はその文字が10進数で表現された数値であることを示すものです。更に「123(10)」という10進数表現は下記のような構成で数を表現しています。
(102 × 1) + (101× 2) + (100 × 3) = 123(10)
これは各桁に相当する数値(102、101、100)がいくつあるかで数を表現していることを示しています。この桁に相当する数値に10の累乗を用いて表現したものが10進数での数の表現方法になります。
次に2進数での数の表現を考えてみます。2進数は「0」「1」の2種のいずれかを使用して数を表現します。例えば123(10)の場合、2進数表現だと「01111011(2)」となります。
(27 × 0) + (26× 1) + (25 × 1) + (24 × 1) + (23× 1) + (22 × 0) + (21 × 1) + (20 × 1) = 01111011(2)
10進数と同じく各桁に相当する数値(27(128)、106(64)…)がいくつあるかで数を表現しています。この桁に相当する数値に2の累乗を用いて表現したものが2進数での数の表現方法です。
10進数における桁に相当するものはコンピュータの世界ではビットと呼称されます。つまり上記の場合は123(10)を8ビットの2進数で表現しているということになります。
最上位のビットが0((27 × 0))なので、123(10)は7ビットで表現可能ではあるのですが、ビットのみだと単位が細かすぎるため、基本的には8ビットをひとつにまとめた単位としてバイトという単位で表現することが多いです。そのため、123(10)は1バイト(8ビット)で表現可能で、2進数表現だと「01111011(2)」となります。この時1バイトで表現できる数の最小は0(10)「00000000(2)」で最大は255(10)「11111111(2)」となります。0~255の256パターンの表現が可能と言い換えることもできます。
留意する点としては、コンピュータで扱う2進数表現には桁数に制限があるということです。
2進数(負の整数)
10進数で負の数を表現したい時、先頭に符号をつけて「-123(10)」というように表現します。2進数でも同じように最上位ビットを符号を表すために使用し、その他で量を表す表現方法がありますが、ここでは主に使用されている2の補数表現による負の数の表現について記載します。
まず、10nの補数(nは桁数)というものについて考えてみます。ここでコンピュータの世界と同様に桁数に制限があるとし最大1桁しか使えないものとします。つまり101の補数というものについて考えます。この時下記が成り立つ関係にある数のことを補数と言います。
x + y = 101
例えばxが4だとyは6になります。4と6はお互いに101を補う数として、6の101の補数は4、4の101の補数は6と言い換えることもできます。4と6は足すと実際には10となり2桁になりますが、ここでは前提として10進数もコンピュータの中での2進数と同様に1桁の制限を設けているため、値は1桁目の値である0となります。数式として書くと少しおかしな記述にはなりますが、下記の関係が成り立ちます。
4 + 6 = 0
この関係が成り立つとき、
4 = -6
ということが言えます。
あくまで1桁という制限をかけた世界での話ですが4は-6に相当し、6は-4に相当します。つまり、負の整数を正の整数として表現することが可能になります。これの何がメリットかというと引き算の計算に関してです。例えば6-4=2となりますが、この世界では6+6と書き換えることができ、結果は12になりますが、1桁制限の世界のため2を導出できます。つまり、引き算を足し算に置き換えて導出することが可能になります。
ここまでの内容を元に2進数での28の補数(8ビットの補数)というものについても考えてみます。
x + y = 28 (100000000(2))
例えばxが00001111(2)だとyは11110001(2)になります。
8ビットという制限の中では同じように下記の関係が成り立ちます。
00001111(2) + 11110001(2) = 00000000(2)
この関係が成り立つとき、
00001111(2) = -11110001(2)
ということが言えます。00001111(2)は15(10)、11110001(2)は241(10)つまり241(10)を減算したければその補数である15(10)を加算、15(10)を減算したければその補数である241(10)を加算すればよいということになります。
ちなみに00001111(2)の28の補数の導出方法に関してですが、全てのビットを反転させて(11110000(2))、1を足す(11110001(2))という計算で求めることができます。全てのビットを反転させただけだと足した時にギリギリ28の補数とするには1足りないのでそこに1を加える必要があります。
2進数(実数)
まずは10進数表現での1.75(10)という数値に関して考えてみます。整数の時と同じように10の累乗を用いて下記のように表現しています。整数部分と小数部分を区別する位置を.(ピリオド)で表現します。
(100 × 1) + (10-1 × 7) + (10-2 × 5) = 1.75(10)
同じように1.75(10)を2の累乗を用いて8ビットの2進数で表現することを考えます。
ここで整数部分と小数部分を区別するために整数部分を上位4ビット、小数部分を下位4ビットを使うこととします。
(23× 0) + (22 × 0) + (21 × 0) + (20 × 1) + (2-1 × 1) + (2-2 × 1) + (2-3 × 0) + (2-4 × 0) = 0001.1100(2)
1.75(10)だと8ビットの2進数でも上記のように問題なく表現できますが、0.1(10)を同じように8ビットの2進数で表現しようとすると下記のようになります。
(23× 0) + (22 × 0) + (21 × 0) + (20 × 0) + (2-1 × 0) + (2-2 × 0) + (2-3 × 0) + (2-4 × 1) = 0000.0001(2)
0000.0001(2)は0.0625(10)です。
仮に8ビットの全てを小数部分の表現に使用した場合でも
(2-1 × 0) + (2-2 × 0) + (2-3 × 0) + (2-4 × 1) + (2-5 × 1) + (2-6 × 0) + (2-7 × 0) + (2-8 × 1) = 0.00011001(2)
0.00011001(2)は0.09765625(10)です。上記の通り使用できるビット数を増やせば増やすほど精度が上がり、0.1(10)に近づきはしますが、0.1(10)を2進数で完全に表現することは出来ないです。つまり、コンピュータで扱う2進数表現は桁数(ビット数)の制限があるため、実数を扱う場合は正確な値ではなく近似値であるということを理解しておく必要があるということです。仮に上記のルールで実数を扱うデータのビット数が8ビットしかない場合0.1(10)を入力した瞬間0.0625(10)になってしまうということです。
2進数(浮動小数点表現 – 考え方)
浮動小数点表現は数値を符号と係数と基数と指数の組み合わせで数を表現する方法です。「符号 係数 × 基数指数」で数値を表現します。例えば123(10)の場合は下記のように表現されます。
1.23 × 102 = 123
コンピュータの世界では2進数表現となりますので、2進数であることをわざわざ表現する必要はありません。つまり基数は必要ではなく、符号と係数と指数のみを用いて数を表現します。
ここでデータの大きさを8ビットとして、最上位ビットを符号、続けて3ビットを指数、続けて4ビットを係数を表現するビットとして使うこととします。更に指数の3ビットの内、1ビットを符号、3ビットで数値を表現し、係数4ビットの内、1ビットで整数部、3ビットで小数部を表現するルールとします(これは仮のルールです)。
まず、0.1(10)であれば下記のように表現できます。
0.1(10) = 0.0001100110011…(2) = 0.1100110011…(2) × 2-3
これを上記のルールに従ってビット変換すると、まず符号は正なので0、続けて指数の-3(10)は111(2)、係数は上位4ビットが0110(2)なのでこの4ビットには0110(2)を格納します。つまり、「01110110(2)」となります。これは上記の仮のルールに従って表現されたビット列だと解釈すると0110(2)の2-3ということになるので0.000110(2)つまり0.09375(10)を表現していると理解できます。
あくまで考え方の紹介ということなので実際にはより複雑、かつ限られたビット数の中で最大限精度を高めるための様々な工夫がされております。例えば上記で指数の-3(10)を更に符号部と値に分けて3ビットに格納しましたが実際にはそのようなルールで指数部のビット列に格納されてはいないのでご注意ください。
本項目の説明での重要な点は限られたビット数の中で符号、指数、係数それぞれを区分けし、その区分けされた値を指定のルールでビット変換して数を表現するのが浮動小数点表現の考え方だということです。
2進数(浮動小数点表現 – IEEE 754)
限られたビット数の中で符号、指数、係数それぞれを区分けし、その区分けされた値を指定のルールでビット変換して数を表現するのが浮動小数点表現の考え方ですが、この指定のルールとしてIEEE 754が定義されています。
IEEE 754は浮動小数点表現の標準規格(ルール)で、ほとんどのコンピュータでの浮動小数点計算で従っている標準規格になります。例えばこの標準規格においては、限られたビット数で最大限精度を上げるための様々な工夫がされています。再度0.1(10)について考えてみます。
0.1(10) = 0.0001100110011…(2) = 0.1100110011…(2) × 2-3 = 1.100110011…(2) × 2-4
基本的には値が0(10)ではない限りどこかで1は必ず出てくるはずなので、係数は必ず1.xxxxという形になります。つまりこの条件下では基数である2を表現する必要がなく省略したように最上部の1を省略できるということになります。1.xxxxの形になるまで変形すると係数は1100…(2)ですが、最上位の1を省略して1001…(2)となります。このビット列の実際の解釈は1.1001…(2)になります。これで係数を表現できるビット数が1増えることになるため数の精度を上げることができます。これを含めその他様々な規格により実用に足る2進数表現がコンピュータで実現されています。
プログラミングにおける実数の取り扱い
C/C++の基本的なdouble型のサイズは8バイト(64ビット)です。もちろん8バイトという制限がありますのでこのビット数で表現できない数であれば必ず誤差がでます。そのため実数同士の計算を行うとその結果が真の値ではなく近似値になっている可能性があります。そのため、実数の計算においてはこの誤差を念頭に置いてプログラミングを行う必要があります。
誤差が確認できるコードとその実行結果を記載します。
#include <stdio.h>
int main(void) {
double dval = 0.1;
printf("dval = %.17f \n", dval);
return 0;
}
dval = 0.10000000000000001
これまでの説明で0.1が8ビットの2進数では完全に表現できないことを示せていると思いますが、実は0.1を2進数で表現すると「0.0001100110011…(2)」と無限に0011が繰り返されるため、0.1は2進数で真の値を表現することはできない数になります。もちろん64ビットでも同様です。実際に上記のコードを実行すると小数点以下17桁目で誤差が出ていることが確認できます。
私自身見かけたことがあるのですが、下記のようなコードについても同様です。
#include <stdio.h>
int main(void) {
double dval = 0.1f;
printf("dval = %.17f \n", dval);
return 0;
}
dval = 0.10000000149011612
0.1fはfloat型の0.1を意味します。これをdouble型変数の初期化値としているのですが、0.1で初期化したときより大きな誤差が出ていることが確認できます。一般的なfloat型のサイズは4バイト(32ビット)のため、double型より精度が悪いです。つまり、0.1をfloat型で表現した時の値の誤差はdouble型で表現した時の値の誤差より大きいことになります。そのより誤差の大きいfloat型の値をより高精度なdouble型に入れることでその誤差の値が明確に出てしまっております。このコードを記述した開発者の意図がdvalというdouble型の変数に限りなく0.1に近い値を初期値として入れたいということであれば0.1fでの初期化は上記の通り誤った記述と言えます。
また、今までfloat型で実数の計算を行っていたにもかかわらず最終的な計算結果をdouble型に入れているようなコードがあった場合も注意が必要です。最終的な計算結果をdouble型という高精度の変数に入れているにもかかわらず計算過程で精度の悪いfloat型が使われている場合、計算結果が本来のdouble型の精度で算出出来ていない可能性があります。この場合、計算過程ではfloat型ではなくdouble型を使用するべきではないかといった検討が必要になります。
まとめ
コンピュータでの2進数表現についてまとめてみました。
本記事の内容が訪問頂いた貴方のお役に立てれば幸いです。
内容の正確性には可能な限り配慮しておりますが、誤りや見落としが含まれる場合があります。
また、学習の進行や考え方の変化により記事の内容は随時加筆・修正される場合がありますので、あらかじめご了承いただけますと幸いです。