高度なプログラミングの根底には「数学」があります。検索アルゴリズム、ゲームの物理エンジン、AIといったプログラムの良し悪しは数学を理解しているかどうかで決まります。新刊『 プログラミングのための数学大全 』から「コンピューターはどのように数値を扱うのか」を抜粋して紹介します。第2回は数値表現のしくみについて、具体的には整数と浮動小数点数を説明します。

画像:あんみつ姫/stock.adobe.com
画像:あんみつ姫/stock.adobe.com
画像のクリックで拡大表示

  前回 はコンピューターが数値を扱ううえで基本となる「基数」を紹介しました。今回は、コンピューターが基数(で表現された数)をどのように扱うのかを見ていきましょう。

 最初に、コンピューターは73,939,133や3.141592といった数をどのように表しているのかを考えてみましょう。73,939,133は、存在しうる最長の右切り捨て可能素数[素数の各位を右から順に切り捨てた数字すべてが素数である数]です。整数なので、コンピューターは正確に示すことができます。一方の3.141592は実数[有理数と無理数を合わせた数]です。コンピューターは完全に正確な形では示せません(これについては後ほど説明します)。

 コンピューターは、整数と浮動小数点数を格納する限られた数のビットに数値を割り当てます。通常、ビットの数は2の累乗数、つまり8、16、32、64などです。数値は2進数形式で格納されます。コンピューターのビット数を超えないかぎり、整数は2進数の桁数がそのまま必要なビット数を示します。厄介なのは浮動小数点数です。まずは整数から見ていきましょう。

整数を表現する

 整数を2進数に変換するアルゴリズムは 前回 に紹介済みなので、次の式は理解できるでしょう。

画像のクリックで拡大表示

 単純に見えますが、最初の2つは8ビット、あとの2つは11ビットの2進数であることに注意してください。8ビットのデータ型しか格納できないシステムでは、1,066や1,963には気の毒ですが、先頭の8ビットまでしか入れてもらえません(桁数は右から左に数えることに注意)。そのため1,066は001010102、1,963は101010112にされてしまいます。

 これが、みなさんに最初にお見せする数学とコンピューターとの衝突現場です。コンピューターのメモリー容量には制限があるため、その範囲内の数値しか扱えません。C言語などほとんどのプログラミング言語は、整数変数のサイズの指定が求められ、そこから使用可能な整数の範囲が定義されます。Pythonなど一部の言語では、整数の格納に使えるかぎりのメモリーを割り当てることができるので、プログラマーの手間が省けます。

 nビットの2進数は、[0,2n-1]の値を格納できます。つまり、表1-2に示すとおり、データ型ごとに割り当てられる範囲が異なるということです。

画像のクリックで拡大表示

 一般的なプログラミングで64ビット以上の整数が必要になることはまずありません。32ビットあれば十分です。よし、これで整数を2進数で格納できる……と安心するのは早計です。重大な見落としにお気づきでしょうか。負の数値です。これまでに登場したのは正の数、または符号なしの数でした。

 負の数値はどのように格納すればいいのでしょう。負数の格納に試行錯誤を重ねてきたコンピューターエンジニアたちが落ち着いた先は、「2の補数」でした。2の補数のいちばんの強みは、コンピューターから見ると引き算が足し算になるという点です。つまり、2つの整数の引き算を、2つめの値を負にした足し算にするのです。整数の2の補数を求めるには、2進数で正の値を書き、すべてのビットを逆転させます。つまり、1を0に、0を1にします。

 たとえば-42なら、42を2進数に変換して(001010102)、ビットを逆転させて110101012とします。それに1を加えて110101102にします。次の式を見てください。1+1=102なので、1を繰り上げています。

画像のクリックで拡大表示

 ここでちょっとお断りです。まず、私は8ビットの2進数を使っているため、42を2進数で表すよう頭にゼロが2つ付いています(001010102)。次に、最上位ビット(いちばん左)が1だということです(110101012)。2の補数で数値を格納するときは、指定した整数のサイズ(ここでは8ビット)のいちばん左のビットは常に、負なら1、正なら0になります。

 2の補数アルゴリズムを2回適用すると-42は42に戻るはずです。試してみましょう。まず、110101102を001010012にします。これに1を足せば、期待どおり001010102=42が得られます。

 では、-42と42の2進数を足すとどうなるでしょうか。

画像のクリックで拡大表示

 0になると思ったら、9ビットの2進数になってしまいました。しかし、前に話したとおり、8ビットの枠に1066を格納しようとしても、下の8桁だけしか入りません。この場合、下の8桁はすべてゼロなので、答えは予定どおりゼロとなります。

 符号付き整数と呼ばれる正または負の整数を格納する場合は、犠牲を伴います。いちばん左の1ビットは正か負かを示す符号のために使われるので、実質的に半分の大きさの数しか表せなくなります。符号なしのnビット整数の範囲は[0,2n-1]ですが、2の補数を使うと[-2n-1,2n-1-1]となります。8ビット整数なら[-128,127]の範囲です。16ビット整数なら[-32,768,32,767]で、32ビット整数なら[-2,147,483,648,2,147,483,647]です。

 このように、nビット列には2つの表し方があります。符号なしのビット列ならば、それは[0,2n-1]の正の整数です。2の補数の形式の符号付きビット列と思われるときは、[-2n-1,2n-1-1]の正または負の値となります。ビット列の解釈は、全体の文脈から読み取る必要があるのです。

 片付けなければならない怪物は、もう1匹います。次のビットパターンを考えてください。

画像のクリックで拡大表示

 これはある整数の16ビットのパターンですが、符号付きか符号なしかで-13,613か51,923かのいずれかになります。しかし、コンピューターはどうやってこの16ビットをメモリーに格納するのでしょう。16ビットの整数を格納するには2バイトのメモリーが必要です。

 賢明なみなさんなら、片方に110010102を、もう片方に110100112を入れるとお考えでしょう。たとえば、2つの空きメモリーが隣接していて、1177と1178を格納するなら、こうなります。

画像のクリックで拡大表示

 これもありです。

画像のクリックで拡大表示

 上位バイトから下位バイトの順番にメモリーの下位から格納するか、あるいはその逆か。ビッグエンディアンか、リトルエンディアンか、です。「エンディアン」とは、数値をバイトごとにメモリーに格納する方式です。上位バイトを先に格納すれば「ビッグエンディアン」になります。「ネットワークオーダー」とも言います。下位のバイトから格納すれば「リトルエンディアン」になります。

 通常これは、コンピューターのCPU(中央演算処理装置)により決まります。たとえば、インテルとAMDのCPUはリトルエンディアン方式なので、4バイトのメモリーを使う数値の場合、最初のバイト(メモリーアドレスのいちばん下)に最下位の8ビットが入ります。プログラマーがエンディアンを意識することはまずありませんが、そいつは突如その醜い頭をもたげて背中に噛みついてきます。とくに注意すべきは、別のシステムで書かれたバイナリーファイルを読み込むときや、センサーからデータを取得するときなど、ネットワーク上でデータを移動するときです。

 32ビット数は、符号のあるなしにかかわらず、4バイトのメモリーを使用します。ビッグエンディアンでは、メモリーアドレスの下から上へと順番にバイトを格納していきます。表1-3を見てください。

画像のクリックで拡大表示

 リトルエンディアンの場合は表1-4のようになります。

画像のクリックで拡大表示

浮動小数点数を表現する

 整数の表現は比較的簡単にできますが、実数を浮動小数点数として表すのは大変です。長い歴史をたどると、IEEE(米国電気電子技術者協会)754[浮動小数点数をコンピューター上で効率的かつ正確に表現するための国際規格]に行きあたります。これは、ほぼあらゆる場所で使われている浮動小数点数の規格で、ごく一般的な「バイナリー32(binary32)」や「バイナリー64(binary64)」など、複数のサイズの浮動小数点数が定義されています。

 C言語では、それぞれfloatとdoubleというデータ型になります。Pythonでは、標準のPythonインタープリターがC言語で書かれているため、すべての浮動小数点数は実質的にdoubleです。名前が示すとおり、binary32は32ビット、binary64は64ビットを使用します。私たちが愛用しているPythonの科学技術計算のためのライブラリーNumPyでは、これらをfloat32、float64と呼んでいます。

 まずは32ビットの浮動小数点数から見てみましょう。図1-1はビットごとの配列を示しています。

画像のクリックで拡大表示

 左端のビット31は符号ビットです。0が正で1が負です。次の8ビット(E)は符号なし8ビット数として格納される指数です。残りの23ビット(M)は「仮数」です。

 実際の数値は科学的記数法で格納されますが、2進数では次のようになります。

画像のクリックで拡大表示

 仮数には暗黙の1が想定されます。b22からb0はビット22からビット0までを表しますが、2の指数は10進数です。この指数は、ビット23から30までの値(E)から127を引くと得られます。

 科学的記数法では基数10を使用しますが、基数2の科学的記数法も確認しておきましょう。リスト1に示すC言語のプログラムを見てください

画像のクリックで拡大表示

 これにより、次の浮動小数点数が出力されます。

画像のクリックで拡大表示

 このプログラムではvを2.718に設定していますが、結果は2.71799994と表示されます。この問題については後で考えるとして、今は1番目に表示された数、402DF3B616に集中しましょう。2進数で書き表すと、次のようになります。

画像のクリックで拡大表示

 符号、指数、仮数の部分はスペースで区切っています。符号ビットは0なので正の数です。指数は128。ここから127を引くと1となるため、仮数がなんであれ、21を掛けることになります。

 仮数はどのように求めるのでしょうか。仮数に暗黙の1ビットがありましたね。これは、仮数がどんな値になろうとも1を足すということです。仮数自体は、つねに1より小さい数になります。

 仮数が基数10なら、桁が右に移動するごとに10をマイナス乗するときの指数が変わります。今回のように基数2なら2のマイナス乗です。たとえば、仮数の最初のビットが2-2なら、右隣の桁では2-4……と続きます。すべてのビットの値を合計して1を足して21=2を掛けると、C言語のコードが示した値になるはずです。ためしにPythonで合計、加算、2の累乗をしたところ、2.7179999351501465という結果になりました。これを小数点8位で丸めると、C言語と同じ2.71799994になります。

 お見事! ……とはいきません。vはぴったり2.718になるはずなのに、違っています。これは有限なメモリーの仕業です。仮数部には23ビットしかないため、実数をすべて格納できません。そのため、2.718が「もっとも近い表現可能な値」として示されるのです。もっとも近い表現可能な値にするために数値を丸めたことで生じる誤差を「丸め誤差」と呼びます。浮動小数点数を扱うかぎり、この丸め誤差とは縁が切れません。

 ある基数では有限小数だった数値が、別の基数ではそうでないことがあるので注意が必要です。たとえば、有限小数の0.1が0.000112になることがあります。下線部分が無限に繰り返されてしまうため、浮動小数点数では0.1を正しく表現できないのです。ためしに、PythonかC言語で、0.1に0.1を100回足してみてください。10.0になりましたか?C言語はPythonよりも正確ですが、それはdoubleとfloatのどちらでしたか?

 最後に、32ビット浮動小数点数は図1-1のように格納されますが、64ビットも基本的には同じです。ただし、指数部は11ビット(1,023を引く)、仮数部は52ビットです。仮数部の値に1を加えるのをお忘れなく。

良いコードを書くために必要な数学を学ぼう

数学とプログラミングには深い結びつきがあります。本書を読めば、基本的なコーディングから本格的なソフトウェア開発までに欠かせない「数学全般」が一気に学べます。

ロナルド・T・クナイスル(著)、井上卓馬(監訳)、金井哲夫(翻訳)/日経BP/5500円(税込み)