本書の間違いに気づいた場合は、ぜひ教えてください。
1. この章の内容
この章では、コンピューター内部での数の表現を扱います。整数については負の数の表し方(2の補数表現)と桁あふれ、実数については浮動小数点表現と、それに伴う誤差を学びます。(基本情報技術者試験などでも頻出の内容です。)
2進法・16進法の読み方や、10進法との相互の変換(基数変換)は、この章の前提です。忘れている人は、先に補足資料 数の表現:2進法と基数変換 で確認してください。この章では、基数を右下の括弧で示し、\(1010_{(2)}=10_{(10)}\) のように書きます。
2. プログラミング言語による整数の2進表現と16進表現
C, Python, Ruby, Julia, JavaScript, Java 等の多くのプログラミング言語では、2進法の \(1010_{(2)}\) を 0b1010 と表記します(C言語の2進リテラルはC23規格からの標準対応です)。b は2進を表す binary の頭文字です。また、16進法の \(\mathrm{AB}_{(16)}\) は 0xab と表記します(大文字の 0xAB でもかまいません)。x は16進を表す hexadecimal からです。
2.1. Python
>>> 0b11101010
234
>>> 0xea
234
>>> bin(234)
'0b11101010'
>>> hex(234)
'0xea'
2.2. Ruby
irb> 0b11101010
234
irb> 0xea
234
irb> 234.to_s(2)
"11101010"
irb> 234.to_s(16)
"ea"
2.3. Julia
julia> Int(0b11101010)
234
julia> Int(0xea)
234
julia> string(234, base=2)
"11101010"
julia> string(234, base=16)
"ea"
2.4. JavaScript
> 0b11101010
234
> 0xea
234
> (234).toString(2)
'11101010'
> (234).toString(16)
'ea'
多くのプログラミング言語では、標準の関数で2進法や16進法の文字列に変換できるのは整数だけです。JavaScript の toString(2) は小数にも使え、たとえば (0.625).toString(2) は '0.101' になります。
3. コンピューター内での数の表現
ここでは、コンピューター内での数の表現について考えます。コンピューター内部では、0と1のみで数を表現する必要があります。0または正の整数については、符号なし整数として素直に2進法で表現することができます。このような場合、1つの整数に \(n\) ビット用いる場合は、\(0\sim 2^n-1\) の範囲の整数が表現できます。
たとえば、プログラミング言語Cでは、 unsigned int という型宣言により、多くの処理系では32ビットの符号なし整数を扱うことができます(\(0\sim 2^{32}-1\)の範囲の整数)。
一方で、負の数を含む整数表現については、歴史的には様々な手法が提案されてきました。ここでは、「符号付き絶対値表現」と、現在主流となっている「2の補数表現」について説明します。
3.1. 符号付き絶対値表現
コンピューター内では、負の整数をどう表現するのでしょうか。もっとも素直に思いつくのが、正負を表す符号のために1ビットを使う方法です。コンピューター内で1つの整数表現に \(n\) ビットを使うとすると、一番左側の1ビット(最上位ビット)を正負の符号に用い、\(n-1\) ビットを絶対値の表現につかう方法です。最上位ビットが1のとき負、0のとき正を表します。(符号付き絶対値表現をした2進法による表記には、下付きの「(2s)」をつけて表すことにします。)
-
整数として4ビットを用いる場合、下位の3ビットで整数の絶対値を表す(つまり、0〜7)
-
最上位の1ビットを用いて符号を表す
たとえば、4ビットを用いて \(-5\) を表現する際には、負なので最上位ビットを1として、絶対値5を残りの3ビットで表現します。
| 表現したい数 | 符号付き絶対値表現 |
|---|---|
−0 |
1000 |
−1 |
1001 |
−2 |
1010 |
−3 |
1011 |
−4 |
1100 |
−5 |
1101 |
−6 |
1110 |
−7 |
1111 |
0 |
0000 |
1 |
0001 |
2 |
0010 |
3 |
0011 |
4 |
0100 |
5 |
0101 |
6 |
0110 |
7 |
0111 |
この表現の問題点は、\(1000_{\text{(2s)}}=-0\) と \(0000_{\text{(2s)}}=+0\) のように、0の表現が2通りになってしまうことです。また、正の整数と負の整数を素直に加算することができません。\(3 + (-2)\) を単純に計算すると下記のように \(-5\) になってしまいます。
そのため、整数の正負や演算ごとに場合分けをして加算回路と減算回路を準備しておく必要があります。
3.2. 補数表現による負の数の表現
2の補数表現では、負の数を含む足し算も、符号なし整数と同じ足し算の仕組みで計算できます。符号付き絶対値表現では符号に応じて足すか引くかを選ぶ必要がありましたが、ここでは負の数の表し方そのものを工夫します。
|
この節の読み方
まずメスシリンダーで仕組みをつかみ、次に容量を16に変えて4ビットの2の補数表現へ進みます。「桁上げと桁あふれを区別する」までが基本です。後の「発展:合同式で確かめる」は、読み飛ばして浮動小数点表現へ進めます。 |
まず、身近な例でこのような表現方法を考えてみましょう。ここでは、0から10までの目盛りが入った容量10のメスシリンダーを使い、水位は整数だけを考えます。計算の途中で満杯になった1本は取り除くので、数を記録するときの水位は0〜9の10通りです。メスシリンダーにマイナスの量の水を入れることはできませんから、水の量は常に0以上です。この状況で、どうすれば負の数を表せるでしょうか。
アイデア: 「上からの空き」で負の数を表す
発想を転換して、メスシリンダーを上から見てみます。水位6まで水が入っているとき、すりきり一杯(10)までの空きは \(10-6=4\) です。この「あと4足りない」という空きの量を、負の数 \(-4\) の絶対値とみなすことにします。つまり、
-
正の数は、水位をそのまま読む
-
負の数は、上からの空き(10−水位)にマイナスを付けて読む
という2通りの読み方をするのです。
一般に、\(y=10-x\) の関係にある \(x\) と \(y\) について、
\(x\) の10の補数は \(y\) である
といいます。負の数 \(-4\) を、絶対値4の10の補数である6(水位6)で表す方法が補数表現です。コンピューター内部の「水位」にあたるものが、0以上の整数(ビット列)です。
正負の境目を決める
ところで、水位6を見たとき、「正の数6」と読むのか「空き4、つまり \(-4\)」と読むのかは、どうやって区別するのでしょうか。ここは取り決めが必要なところです。この章では、
-
水位0〜4 → 0または正の数として、水位をそのまま読む
-
水位5〜9 → 負の数として、空きにマイナスを付けて読む
と取り決めます。水位10は計算途中の満杯の状態です。満杯の1本を取り除き、空のメスシリンダー(水位0)を結果として残します。図 2に、水位ごとの読み方をまとめました。
この取り決めで、\(-5\) から \(4\) までの10通りの整数が表せるようになりました(結果を記録する水位は0〜9なので、0の表現も水位0の1通りです)。
境目は取り決めですが、ここでは正と負にほぼ半分ずつ割り当てました。後で2進法に移ると、この分け方によって「先頭のビットだけで正負がわかる」という利点が生まれます。
足し算してみる: 「注ぎ足すだけ」で引き算までできる
この表現の威力は足し算で発揮されます。負の数が混ざっていても、対応する水位の水をただ注ぎ足すだけでよいのです。引き算のための特別な仕組みは要りません。ルールは1つだけです。あふれたら、あふれた分を2本目のメスシリンダーに受けて、満杯になった1本目(=10)は捨てます。
まず、\((-4)+3\) を計算してみます。\(-4\) は水位6、\(3\) は水位3で用意し、注ぎ合わせると水位9になります。9は負の領域ですから、空き1を読んで答えは \(-1\)。正解です(図 3)。
次に \((-3)+4\) です。\(7+4=11\) となるので水があふれます。あふれた分1を2本目に受け、満杯の1本目は捨てます。2本目に残った水位1が答えです。\(10\) を捨てる操作が、ちょうど \(11-10=1\) の計算になっています(図 4)。
負の数同士の \((-1)+(-2)\) も、まったく同じ手順で計算できます。\(9+8=17\) であふれ、2本目に残るのは水位7。7は負の領域なので空き3を読んで、答えは \(-3\) です(図 5)。
\((-3)+3\) のような「足すと0になる」組合せでは、ちょうど満杯になります。満杯になった1本を取り除くと、残りの水はありません。空のメスシリンダー、水位0が答えです。互いに打ち消し合う数を足すと0になる性質が、水の操作に対応しています(図 6)。
うまくいかない場合: 桁あふれ
では、\(3+4\) はどうなるでしょうか。水位は7になり、一見正しく計算できているように見えます。しかし、取り決めにより水位7は負の領域です。空き3を読むと、\(3+4=-3\) という、あり得ない結果になってしまいました(図 7)。
原因は答えの側にあります。正しい答え7が、この取り決めで表せる範囲 \(-5\sim 4\) からはみ出しているのです。このように、計算結果があらかじめ取り決めた範囲を超えてしまうことで発生する誤りを「桁あふれ」と呼びます。ここでは水はあふれていません。それでも、数としては桁あふれです。逆に、水があふれた先ほどの \((-3)+4\) は正しく計算できました。水があふれることと、数の桁あふれは別のことなのです。結果が範囲内に収まっている限り、「注ぎ足すだけ」の仕組みは常に正しく働きます。
3.3. 2の補数表現
コンピューター内部で使われる2の補数表現は、前節のメスシリンダーの仕組みをそのまま2進法に置き換えたものです。まず容量を16に変えてみましょう。水位0〜15は、4ビットの 0000〜1111 に対応します。0〜7をそのまま読み、8〜15は「16までの空き」にマイナスを付けて読むと、−8〜7が表せます。一般に、\(n\) ビットでは容量 \(2^n\) と考えます。対応関係を先に頭に入れておくと、この節の内容はすべてメスシリンダーの話の言い換えとして読めます。
メスシリンダー(容量10) |
2の補数表現(\(n\)ビット) |
容量10 |
\(2^n\) |
記録する水位(0〜9) |
ビット列を符号なし整数として読んだ値 |
空き(10−水位)で負の数を表す |
2の補数(\(2^n-m\))で \(-m\) を表す |
水位5以上を負の数の領域とする |
最上位ビットが1なら負の数とみなす |
満杯の1本を取り除き、残りの水位を読む |
\(n+1\)ビット目への桁上げは無視する |
負の数 \(-m\) を表す水位は、容量から \(m\) を引いた \(2^n-m\) です。固定した \(n\) ビットで、この値を \(m\) の2の補数と呼びます。たとえば4ビットで \(-5\) を表すには、\(16-5=11\)、つまり 1011 を使います。なお、0の2の補数は \(2^n-0=2^n\) ですが、これは \(n\) ビットに収まらない満杯の状態なので、満杯の1本を取り除いて0とします。
一般に、\(n\) ビットの2の補数表現の範囲は
です。最上位ビットが0なら0または正、1なら負と判断できます。この教材では、ビット列をこの規則で読むことを下付きの「(2c)」で示し、符号なしの2進数として読む場合の「(2)」と区別します。
表現したい数 |
2の補数表現 |
「2の補数表現」を符号なし2進表現として解釈したときの数 |
−8 |
1000 |
8 |
−7 |
1001 |
9 |
−6 |
1010 |
10 |
−5 |
1011 |
11 |
−4 |
1100 |
12 |
−3 |
1101 |
13 |
−2 |
1110 |
14 |
−1 |
1111 |
15 |
0 |
0000 |
0 |
1 |
0001 |
1 |
2 |
0010 |
2 |
3 |
0011 |
3 |
4 |
0100 |
4 |
5 |
0101 |
5 |
6 |
0110 |
6 |
7 |
0111 |
7 |
4ビットのビット列16通りを、符号なしの値の順に円環状に並べたものが図 8です。時計の文字盤のように、1を足すと時計回りに1目盛り進み、15の次は0に戻ります(満杯の1本を取り除くことにあたります)。外側の数字は、同じビット列を2の補数表現として読んだ値です。0から時計回りに7まで進むと正の数が並び、その次の 1000 で −8 に切り替わります。
まず、正の整数から、2の補数表現による負の整数の求め方について説明します。 ここでは、整数表現に用いるビット数を4とします。 このとき、\(-5\) の2の補数表現を求めるには、正の整数 \(5\) の2の補数を計算します。つまり、
と計算します。よって、\(11\) は2の補数表現では \(-5\) を表していることになります(表 3参照)。
また、逆に11の2の補数を求めると、\(16-11=5\) です。つまり、「2の補数表現された負の整数」の絶対値を知りたいときは、2の補数を計算すればよいことがわかります。
ところで、コンピューター内部では数は2進法で表現されていますので、2の補数の計算は10進法を使うより2進法のままで計算した方が簡単です。以下のように考えます。
\(2^4=10000_{(2)}\) ですから \(2^4-1\) の2進表現は4つの1が並んだ \(1111_{(2)}\) となります。この値から、5の4ビット2進表現 \(0101_{(2)}\) を引いた結果は、\(0101_{(2)}\) の各桁のビットの0と1を入れ替える(ビットを反転する)ことと等価です。各桁のビットの反転によって、\(1010_{(2)}\) となります。最初に1を引いてしまったので、その分を最後に加えて、\(1011_{(2)}\) が得られます。これで、2の補数表現で、\(-5\) に相当する表記がえられました。これを、\(1011_{\text{(2c)}}=-5\) と書くことにします。
この手続きをまとめると、以下のようになります。
-
5 を2進法に変換 → 0101(2)
-
0101(2) の各桁のビットを反転 → 1010(2)
-
1010(2) に 1 を加算 → 1011(2)
-
\(-5=1011_{\text{(2c)}}\)
逆に、負の数を表すビット列から絶対値を求めるときも、同じ「反転して1を足す」手続きを使います。
なぜこの手続きで \(2^4-5\) が計算できるのかは、買い物のお釣りの計算にたとえるとよくわかります。1000円を出して437円の買い物をしたときのお釣り \(1000-437\) を素直に筆算すると、繰り下がりが連鎖して面倒です。そこで、まず999円から引いて、最後に1円を足します。\(999-437\) は、どの桁も「9−その桁」を書くだけで繰り下がりが決して起きません。2進法ではこの「9−その桁」にあたるものが「1−そのビット」、つまりビットの反転なのです(図 9)。
4ビットを整数表現に用いるとして、\(3 + (-7)\) を計算してみましょう。
-
\(3\) を2進法に変換 → \(0011_{(2)}\)
-
正の整数なのでそのまま2の補数表現とする → \(3=0011_{\text{(2c)}}\)
-
-
\(7\) の2の補数を計算して \(-7\) を表す
-
まず \(7\) を2進法に変換 → \(0111_{(2)}\)
-
\(0111_{(2)}\) の各ビットを反転 → \(1000_{(2)}\)
-
\(1000_{(2)}\) に1を加算 → \(1001_{(2)}\)
-
よって \(-7=1001_{\text{(2c)}}\)
-
-
\(3+(-7)\) を2の補数表現で計算すると次の筆算のようになります(1(2)+1(2)で桁上げが生じることに注意)。
-
\(1100_{\text{(2c)}}\) の最上位ビットが1なので、これは負の整数であることがわかります。 そこで、各ビットを反転して1を足し、その結果を符号なしで読むと絶対値がわかります。
-
\(1100_{(2)}\) の各ビットを反転 → \(0011_{(2)}\)
-
\(0011_{(2)}\) に1を加算 → \(0100_{(2)}\)
-
よって、\(0100_{(2)}=4\) より、\(1100_{\text{(2c)}}=-4\) であることが確認できました
-
桁上げと桁あふれを区別する
4ビットの2の補数表現で表せる範囲は −8〜7 です。計算の正しい答えがこの範囲に入っていれば、下位4ビットを残す足し算で正しく計算できます。範囲から外れることを、符号付き整数の桁あふれ(オーバーフロー)といいます。
一方、5ビット目への桁上げは、水を注ぎ合わせて満杯の1本ができることに対応します。この1本を取り除くこと自体は、計算の失敗ではありません。
| 計算 | ビット列を符号なしで足した結果 | 下位4ビットの読み方 | 正しい答えは範囲内か |
|---|---|---|---|
\(3+(-7)\) |
|
|
はい。正しく計算できる |
\((-3)+5\) |
|
|
はい。桁上げがあっても正しい |
\(1+7\) |
|
|
いいえ。8は範囲外 |
\((-4)+(-5)\) |
|
|
いいえ。−9は範囲外 |
表の空白で区切った先頭の 1 が、取り除く5ビット目です。筆算の途中は符号なしのビット列として計算し、下位4ビットを残してから2の補数表現として読みます。
異符号の2数を足した答えは、その2数の間に収まるため、桁あふれは起きません。同符号の2数を足して結果の符号が変わった場合が、桁あふれです。メスシリンダーの \(3+4\) の例と同じ見方です。
|
ここまでで押さえたいこと
|
桁あふれが起きたときのプログラムの動作は、言語や型によって異なります。ここで学んだ固定ビット幅の計算の仕組みだけから、すべての言語の動作が決まるわけではありません。
データサイエンスや人工知能等の分野で広く使われているプログラミング言語Pythonでは、整数(int)は任意精度で表現されており、メモリなどの資源が許す限り必要な桁数が増えるため、固定幅の整数のような桁あふれは起こりません。
そのため、固定ビット幅の整数を試すには、numpy という数値演算のためのライブラリを使います。以下では numpy が使えることを前提に、Pythonでの桁あふれの例を見てみます。
8ビットの2の補数表現による整数の演算 \(127+1\) と \((-128)+(-1)\) の計算結果です。(np.int8(127) は8ビットの2の補数表現による整数で127を意味しています。)
この例では範囲を超えた結果が8ビットに収められます。NumPyの整数スカラーの桁あふれは、設定によって警告や例外の対象になります。以下では動作を明示するため、警告を表示する設定にしています。
>>> import numpy as np
>>> _ = np.seterr(over="warn")
>>> print(int(np.int8(127) + np.int8(1)))
-128
>>> print(int(np.int8(-128) + np.int8(-1)))
127
>>> print(int(np.int8(100) + np.int8(100)))
-56
(警告の行は省略しています。over="raise" に設定すれば例外が発生します。参照:NumPy seterr。)
最後の例では \(100+100\) が \(-56\) になっています。8ビットの2の補数表現で表せる範囲は \(-128\leq m\leq 127\) であり、\(200\) はこの範囲を超えているためです。
これもメスシリンダーで説明できます。8ビットの2の補数表現は、容量 \(2^8=256\) のメスシリンダーで、水位128以上を負の領域と取り決めたものにあたります。水位100の水どうしを注ぎ合わせると水位200になります。水はあふれていないのに、水位200は境目の128を越えて負の領域に入ってしまっているため、空き \(256-200=56\) を読んで \(-56\) となるのです。容量10のメスシリンダーでの失敗例 \(3+4\) と、まったく同じ仕組みです(図 10)。
次に、科学技術計算やデータ分析で使われているプログラミング言語Juliaでの例です。
(Int8(127) は8ビットの2の補数表現による整数で127を意味しています。また、 bitstring は与えられた値のコンピューター内部での2進法による表現を取り出す関数です。)
Juliaもビット数で定まる値の範囲を超えると、何のエラーも出さずにあふれたビットを無視して結果を返していることがわかります。
julia> Int8(127) + Int8(1)
-128
julia> Int8(-128) + Int8(-1)
127
julia> bitstring(Int8(-128))
"10000000"
julia> bitstring(Int8(-1))
"11111111"
julia> bitstring(Int8(127))
"01111111"
様々なメリットのある2の補数表現ですが、欠点もあります。大小比較がやや複雑になることです。ビット列を符号なし整数としてそのまま比較すると、負の数のほうが大きな値になってしまい、大小関係が一致しないためです。後述する浮動小数点の指数部で、2の補数ではなくバイアス表現が使われるのはこの点を避けるためです。
ここまでは、整数同士の加算で計算結果が表現可能な範囲を超えてしまう「2の補数表現での桁あふれ」を扱ってきました。これに関連して、もう一つよく似た現象として、ビット幅の異なる型へ値を変換する際のオーバーフローがあります。これは2の補数の加算で起こる桁あふれとは仕組みは異なりますが、「定められたビット数で表せる範囲を超えてしまう」という点では同じカテゴリの問題です。次のコラムは、こうした型変換時のオーバーフローが引き起こした有名な実例です。
|
型変換のオーバーフローが起こした大規模事故: アリアン5の打ち上げ失敗
1996年6月4日、アリアン5の初飛行は打ち上げ直後に失敗しました。調査報告によれば、慣性基準装置で水平速度に関連する内部変数を64ビット浮動小数点から16ビット符号付き整数へ変換した際、値が範囲に収まらず例外が発生しました。同じソフトウェアを使う予備装置も停止していたため、切り替えでは救えませんでした。 問題の処理はアリアン4から引き継いだ姿勢の初期調整の機能で、アリアン5では飛行中に必要のないものでした。新しい飛行条件に対して、以前の「この値は範囲を超えない」という想定を確かめ直せていなかったのです。範囲外の値を検出する機能があっても、その後どう対処するかまで設計する必要があります。 |
3.4. 発展:合同式で確かめる(読み飛ばせます)
| ここは数学的な裏付けを知りたい人のための節です。計算方法と桁あふれは、ここまでのメスシリンダーの説明で理解できます。読み飛ばす場合は、次の「実数の浮動小数点表現による近似」へ進んでください。 |
証明を開く:満杯の1本を取り除いても、何が保たれるのか
容量を \(N=2^n\) とします。整数 \(a,b\) の差が \(N\) の倍数のとき、両者は \(N\) を法として合同といい、\(a\equiv b\pmod N\) と書きます。「満杯の何本分かだけ違う」という意味です。たとえば、容量16では \(-5\equiv11\pmod{16}\) です。合同は等号ではなく、\(-5=11\) という意味ではありません。
正負を含む整数 \(a,b\) を記録する水位を \(u,v\) とすると、ある整数 \(q,r\) を使って \(u=a+qN,\ v=b+rN\) と書けます。したがって、
つまり、記録した水位どうしを足しても、本来の和との違いは満杯の整数本分だけです。満杯の1本を取り除く操作も、\(N\) を引くだけなので、この性質を保ちます。これが、正負を分けずに足し算できる理由です。
ただし、合同なだけでは答えは一つに決まりません。 容量16なら、7も−9も23も互いに合同です。2の補数表現では、−8〜7という連続した16個の整数から一つを選んで読みます。この範囲内にあって同じ水位に対応する異なる整数はありません。異なる2数の差の絶対値は16未満で、16の倍数にはならないからです。
よって、本来の答えが表現範囲内にあれば、読み出した値はその答えに一致します。 範囲外なら、同じ水位に対応する別の値を読んでしまいます。これが桁あふれです。
引き算 \(a-b\) も同じで、\(-b\) に対応するビット列を足せば、\(a-b\) と合同な水位が得られます。答えが表現範囲内なら、それが \(a-b\) です。乗算についても、
なので、積の下位 \(n\) ビットは符号なし乗算と共通です。ただし、より広いビット幅で積全体を求めるには符号を考慮する必要があります。通常の整数の割り算には、このような置き換えは一般には使えません。
最小値の符号反転にも注意しましょう。4ビットの−8は 1000 です。反転して1を足しても下位4ビットは 1000 のままです。これは手順の例外ではなく、正しい答えの+8が4ビットの符号付き範囲に入らないためです。+8まで扱いたければ、先にビット幅を広げます。
3.5. 実数の浮動小数点表現による近似
前節までは、整数の表現を考えてきました。ここからは、実数について考えます。コンピューターは有限の桁しか扱えませんので、すべての実数を正確に表現することはできません。たとえば0.5は2進法で正確に表せますが、0.1は有限桁では表せず、近似値になります。そのため、次のような問題が起こります。 C, C++, Java, JavaScript, Python, Ruby, R, Julia など多くのプログラミング言語で、小数を通常の浮動小数点数(後で説明する倍精度)として計算すると、\(1.0 - 0.9\) と \(0.1\) の値が等しくなりません。
実際にPythonで、\(1.0-0.9-0.1\) を計算し、\(1.0-0.9 = 0.1\) が成り立つかどうかを確かめてみます。
前者の結果は0になりません。後者は、等しければ True(真)となるはずが、「等しくない」という意味の False(偽)を返しています。
>>> 1.0 - 0.9 - 0.1
-2.7755575615628914e-17
>>> 1.0 - 0.9 == 0.1
False
同様に、\(0.1+0.2\) と \(0.3-0.2\) を計算し、\(0.3-0.2 = 0.1\) が成り立つかどうかを確かめてみます。やはり、期待と違う結果になります。
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.3 - 0.2
0.09999999999999998
>>> 0.3 - 0.2 == 0.1
False
なぜこのような簡単な計算ができないのでしょうか。コンピューターを使って数値計算をするときには、コンピューター内部での数の表現方法に関する知識がないと、とんでもない計算をしてしまう可能性があります。この節では、コンピューター内部での実数の近似表現である浮動小数点表現について学び、どうして上記のようなことが起こるのかを考えてみます。
浮動小数点(Floating Point)表現
\(-1234000000\) のような数を表すときに0の続く部分は数の情報としては冗長で領域の無駄遣いになってしまいます。また、\(0.0000001234\) のように小数点以下に0が並ぶ小さな数についても同様です。そこで、それぞれ \(-1.234\times 10^9\) および \(1.234\times 10^{-7}\) のように小数点の位置を指数表現によって移動して表現する方法を浮動小数点表現といいます。 ここでは、とくに整数部が必ず一桁で、かつ0以外の値がくるように小数点の位置を調整した数の表現方法を正規化された浮動小数点表現と呼びます。以後、特に断らない限りは、浮動小数点表現は正規化されたものを指すことにします。
例えば、\(-123.4\times 10^7\) は浮動小数点表現ですが、正規化されていません。\(-1.234\times 10^9\) は正規化されています。
浮動小数点表現における、各部位の名称は以下のとおりです。
| このような指数表記のコンピューターでの入力・表示方法については後述します。 |
小数の変換
ところで、コンピューター内部では10進法ではなくて2進法で数を表現しなくてはなりません。 2進法の小数を10進法に変換する方法は、第1章と補足資料 数の表現:2進法と基数変換 で説明しました。まずは、その復習からです。
この逆で、10進法の小数を2進法に変換する場合にはどうすればよいでしょうか。 10進法では、10倍すると全体の桁が左に1つずれて(シフトして)、小数が順番に1の位に現れますので、順に10倍してその値を拾っていけば、小数の各桁の値がすべて一の位に出現することがわかります。同じように、2進法で小数を1つ左にシフトするには2倍すればよいので、これを小数の数が全部1の位にでてくるまで繰り返します。
よって、1の位にでてきた値を、小数第一位から順に並べて、変換完了です。
IEEE 754による浮動小数点表現
ようやく実際にコンピューター内部での小数を含む数字の表現について説明する準備ができました。ここでは、コンピューターの浮動小数点表現の中でもっとも一般に用いられているIEEE754という標準に基づく方法を紹介します。(IEEEは「アイ・トリプル・イー」と読みます。電気・電子・情報分野の技術者が集まる、世界最大規模の学会です。)
IEEE754の規格の細部を扱うことはできませんので、最低限の内容についてここでは触れます。また、一般には浮動小数点表現に32ビット(単精度)や64ビット(倍精度)を用いますが、ここでは簡単のため、16ビット(半精度)の浮動小数点表現を用いて説明します。
|
半精度は説明のための「おもちゃ」ではありません。大量の数値を扱う深層学習では、メモリ消費とデータ転送のコストを抑えるため、精度をあえて落とした半精度(FP16)や、その変種で指数部を単精度と同じ8ビットに広げたbfloat16が広く使われています。GPUなどのAI向け演算装置は、これらの短い形式を高速に処理する専用回路を備えています。 |
まず、先程求めた \(0.59375\) の2進法による表現 \(0.10011_{(2)}\) を正規化された浮動小数点表現で表すと、
となります。
なぜ仮数部を2進表現で 1.xxx の形に正規化するのでしょうか。主に3つの理由があります。
-
表現の一意性
-
例えば 0.5×2¹ と 1.0×2⁰ は同じ値ですが、正規化しないと複数の表現が可能になってしまいます
-
0でない正規化可能な値では、仮数の大きさを1以上2未満にそろえると指数と仮数が一つに決まります(有限桁で表せない実数は丸めて近似します)
-
-
精度の最大化
-
2進表現された数の正規化では、仮数部の最上位ビットが常に1となることが分かっているため、この1を省略して保存できます
-
その分、より多くの有効桁数を表現できます
-
-
比較演算の効率化
-
正の正規化数どうしなら、指数部の比較だけで大まかな大小関係がわかります
-
同じ指数なら仮数部を比較します。負の数では大小が逆になり、0やNaNなどは別に扱います
-
例えば、\(0.0110_{(2)}\times 2^2\) は正規化すると \(1.1000_{(2)}\times2^0\) となります。
16ビットの半精度では、16ビットを「符号部(1ビット)」「指数部(5ビット)」「仮数部(10ビット)」の3つの領域に分けます(図 11)。
-
符号部は正のとき 0、負のとき 1 とします。この例は正の数なので 0 です。
-
指数部は5ビットを用いて \(-14\sim 15\) までの範囲の指数を表現します。指数が \(n\) のとき、\(n+15\) に値を変換してから、2進法の表記に変換します。このような表し方をバイアス表現といいます(2の補数を使わない理由は後で説明します)。ここでは、指数が \(-1\) なので、\(-1+15=14=01110_{(2)}\) となります。(指数部の
00000と11111は、0や無限大などの特別な値のために取ってあります。) -
仮数部は \(1.0011_{(2)}\) ですが、正規化した場合、必ず1の位は1となるので、この1は節約して省きます。すると、残りの小数点以下のビット列は
0011となります。この右側に6ビットの0をつめて、0011000000となります。
このようにして、実数を有限の浮動小数点表現で以下のように表すわけです。
| 符号(1ビット) | 指数(5ビット) | 仮数(10ビット) |
|---|---|---|
|
|
|
全部つなげると、0 01110 0011000000 となります。これが、0.59375 の16ビット浮動小数点表現となります。4ビットずつ区切って16進法で書くと 0011 1000 1100 0000 → 38C0 です。
逆に、ビット列から値を読むときは、符号部を \(s\)、指数部を符号なし整数として読んだ値を \(e\)、仮数部のビット列を \(f\) として、次のように計算します(指数部が 00000 と 11111 以外のとき)。
たとえば 0 01110 0011000000 なら、\(s=0,\ e=14\) なので \(+1.0011_{(2)}\times 2^{14-15}=1.0011_{(2)}\times 2^{-1}=0.59375\) に戻ります。
整数とは表現や計算の方法が大きく異なるため、ほとんどのコンピューターの演算装置は、浮動小数点の計算専用の演算装置を内蔵しています(これを FPU(Floating Point Unit) といいます)。
指数部では、なぜ2の補数表現ではなく、バイアス表現(+15を加算)を使うのでしょうか。
-
比較の簡単化
-
2の補数表現のビット列を符号なし整数として比べると、負の数のほうが大きく見えてしまいます(2の補数表現の節で見た欠点です)。
-
バイアス表現では、指数が大きいほど指数部のビット列も符号なし整数として大きくなります。そのため、符号部が0の浮動小数点数どうし(非数を除く)は、16ビット全体を符号なし整数とみなして比べるだけで大小がわかります。
-
-
特別な値の区別
-
指数部が
00000(最小)と11111(最大)のビット列を、0や無限大などの特別な値のために取っておけます。正規化数の指数部は00001〜11110です。
-
実例で見てみましょう。
-
指数 \(-14\) を表すには: \(\mathbf{-14} + 15 = 1 \rightarrow 00001_{(2)}\)
-
指数 \(0\) を表すには: \(\mathbf{0} + 15 = 15 \rightarrow 01111_{(2)}\)
-
指数 \(+15\) を表すには: \(\mathbf{15} + 15 = 30 \rightarrow 11110_{(2)}\)
ところで、このままでは \(0\) を表現することができません。仮数部の最上位には、省略した \(1\) が必ず隠れているからです。そこで、上記の規則とは別に、表 5の例外的な表現が決められています。
| 値 | 符号(1ビット) | 指数(5ビット) | 仮数(10ビット) |
|---|---|---|---|
正のゼロ(\(+0\)) |
|
|
|
負のゼロ(\(-0\)) |
|
|
|
非正規化数(0に近い小さな値) |
|
|
|
正の無限大(\(+\infty\)) |
|
|
|
負の無限大(\(-\infty\)) |
|
|
|
非数(NaN: Not a Number) |
|
|
|
正のゼロと負のゼロを区別すると、負の小さな値が0に丸められたときにも符号の情報が残ります。たとえば次のように、0で割った結果の符号が変わります。
非数は、\(0/0\) などの未定義の計算結果を表すために用いられます。
指数部が 00000 で仮数部が0以外の値は非正規化数と呼ばれ、0付近の非常に小さい値を細かく表すために用いられます(この教材では詳細は扱いません)。
先程と同様にプログラミング言語Juliaでも確認してみます(NaN16 は半精度の非数です)。
julia> bitstring(Float16(0.59375))
"0011100011000000"
julia> bitstring(Float16(0))
"0000000000000000"
julia> bitstring(Float16(1)/Float16(0)) # +∞
"0111110000000000"
julia> bitstring(NaN16) # NaN
"0111111000000000"
Juliaで扱う浮動小数点表現が、説明と一致していることがわかります。なお、非数は符号部と仮数部の値が一つに決まっていません。\(0/0\) の計算で得られる非数のビット列は、CPUの種類によって異なることがあります(たとえばx86系のCPUでは符号部が1になります)。
コンピューターでの指数表記
コンピューターやExcelなどでは次のような数値表現をよく見かけます。
1.23e-14
これは、科学的記数法(指数表記)と呼ばれる表記方法です。この表記は次のように解釈します。
1.23 が仮数部(significand)、e-14 が「10の−14乗」を意味する指数部です。記号「e」は「exponent」(指数)の頭文字です。
つまり、1.23e-14 は数学的には次のように表されます。
コンピューターやプログラミング言語では、非常に大きな数値や非常に小さな数値を表現する際にこの指数表記が使われます。
-
1.0e-13は \(1.0 \times 10^{-13}\) (0.0000000000001)を表す -
2.5e6は \(2.5 \times 10^6\) (2,500,000)を表す
この表記は、浮動小数点表現を10進法で書いたものです。内部表現(IEEE 754)も「仮数部」と「指数部」に分けて格納する点は同じですが、内部では基数が10ではなく2です。そのため、画面に 0.1 と表示されていても、内部の値が0.1ちょうどとは限りません。この点を次に見ます。
表現誤差
10進法の数値を2進法の浮動小数点表現で表すときの問題点を見てゆきます。 10進法の \(0.1_{(10)}\) を浮動小数点表現することを考えます。 まず、2進法に変換します。
以下のように 0.1(10) を順次2倍して、1の位にでてくる値(0または1)を並べてゆきます。
2行目と6行目に0.2が出現していますので、これ以降は循環することになります。 つまり、0.1(10) は2進法では、1の位にでてきた値を、小数第一位から順に並べると
と無限に続く循環小数になります。
|
10進法では有限桁で表すことができる小数が、2進法では循環小数になってしまう例をみました。では、2進法では有限桁で表すことができる小数が、10進法で表すと循環小数になってしまうことはあるのでしょうか? 考えてみてください。 答えありません。2進法で小数点以下 \(m\) 桁の小数は、ある整数 \(k\) を使って \(\dfrac{k}{2^m}\) と書けます。分母と分子に \(5^m\) を掛けると \(\dfrac{k\times 5^m}{10^m}\) となるので、10進法でも小数点以下 \(m\) 桁以内の有限小数です。たとえば \(0.001_{(2)}=\dfrac{1}{8}=0.125\) です。逆向きがうまくいかないのは、10進法の分母 \(10^m=2^m\times 5^m\) に含まれる因数5を、2のべき乗の分母では表せないからです。 |
10進法の 0.1 を16ビットの浮動小数点表現(半精度)で表してみます。正規化すると \(0.1_{(10)}=1.100110011001\cdots_{(2)}\times 2^{-4}\) なので、
-
符号部は正なので
0 -
指数部は \(-4+15=11=01011_{(2)}\)
-
仮数部は、1の位の1を省略した小数点以下のうち、10ビットしか格納できません。
1001100110の後ろに続くビットは0110…で、捨てる部分は最下位ビットの重みの半分より小さいので、切り捨てた1001100110が最も近い値になります。
となります。IEEE 754の既定の丸め方式は「最も近い表現可能な値に丸める(ちょうど中間のときは最下位ビットが0の方に丸める)」で、この例では切り捨てと同じ結果になります。よって、0.1(10)の16ビット浮動小数点表現は
0 01011 1001100110
となります。つまり、0.1 の2進法表現は無限に続く循環小数なのに、浮動小数点表現では仮数部の10ビット分までしか格納できません。こうして、もとの10進法の値との間に誤差が生じます。このような誤差を表現誤差と呼びます。
実際に \(1.1001100110_{(2)}\times 2^{-4}=0.00011001100110_{(2)}\) を10進法に変換すると 0.0999755859375 となります。半精度で表せる数は飛び飛びで、0.1 の近くでは図 12のように並んでいます。0.1 そのものは表せないので、最も近い 0.0999755859375 が選ばれるのです。
Juliaで実行してみると、たしかに値が一致することがわかります(Python では NumPy の float16 型で同じ値を確認できます)。
julia> using Printf
julia> @printf("%0.16f",Float16(0.1))
0.0999755859375000
ふだんの計算で使われる倍精度(64ビット、仮数部52ビット)でも、仮数部の桁数が増えて誤差が小さくなるだけで、0.1 が正確に表せないことは変わりません。Pythonの Decimal(0.1) で、倍精度の0.1が内部で実際に持っている値を10進法で表示できます。
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
0.1、0.2、0.3 はどれも少しずれた値で格納されているので、\(0.1+0.2\) の結果が、0.3 を格納した値と一致しないことがあります。これが、節の最初に見た現象の原因です。
julia> 0.1 + 0.2
0.30000000000000004 # 0.3ではない
julia> 0.1 + 0.2 == 0.3
false # 等しくないと判定される
他にもコンピューターでの計算には、様々な誤差の要因があります。コンピューターで重要な計算をする際には、誤差がどのように生じるか、きちんと学ぶ必要があります。
|
PythonやJuliaなどのプログラミング言語の多くには、このような2進法による表現誤差が発生しないように10進法の演算を提供するライブラリがあります。金額など、10進の有限小数を正確に扱いたい場合に有用です。入力を文字列で与え、必要な精度と丸め方も設定します。
この例は表現誤差なく計算できます。ただし、10進法でも1/3のような循環小数は有限桁では表せず、演算の精度を超えると丸めが必要です。Decimalを使えば、どんな計算も正確になるという意味ではありません。 |
|
Excelも内部では倍精度の浮動小数点数を使っているので、同じ表現誤差があります。ただし、ごくわずかな差を丸めて扱う処理(最適化)が組み込まれているため、誤差が見えにくくなっています。たとえば |
4. 数値計算における様々な誤差
コンピューターでの数値計算には表現誤差の他にも様々な誤差の要因があります。主な誤差の種類と対策について見ていきます。
4.1. 丸め誤差と数値の吸収
丸め誤差とは
丸め誤差は、計算結果を有限の桁数で表現する際に発生する誤差です。コンピューターでは浮動小数点数で実数を近似するため、表現できない値は最も近い値に丸められます。特に繰り返し計算を行う場合、丸め誤差が蓄積されて大きな問題になることがあります。
julia> s = 0
0
julia> for i=1:10000; s += 0.0001; end
julia> s
0.9999999999999062 # 理論値は1.0
0.0001 は2進法では循環小数なので、格納した時点で表現誤差があります。さらに、足し算のたびに結果が最も近い表現可能な値へ丸められます。1万回の足し算で、これらの小さな誤差が積み重なった結果、1.0 からずれました。
数値の吸収
吸収は丸め誤差の一種で、大きな数と小さな数の足し算・引き算で、小さな数の寄与が失われる現象です。浮動小数点数の仮数部の桁数が限られているために起こります。
仕組みは10進法で考えるとわかりやすいでしょう。有効数字4桁で計算するとき、\(1234+0.4=1234.4\) は4桁に丸めると1234に戻り、0.4 を足した効果が消えます(図 13の左)。
>>> a = 1e16
>>> b = 1.0
>>> result = a + b
>>> print(result == a)
True # 小さな値が「吸収」されている
>>> print(result - a) # 理論値は1.0
0.0 # 小さな値の情報が完全に失われている
倍精度の仮数部は2進法で53桁(省略した先頭の1を含む)で、10進法ではおよそ16桁に相当します。\(10^{16}\) 付近では、倍精度で表せる隣り合う数の間隔が2になっています。\(10^{16}+1\) はちょうど \(10^{16}\) と \(10^{16}+2\) の中間にあたり、丸めの規則(中間なら最下位ビットが0の方)で 1e16 に戻されます。
たくさんの数を足すときは、小さい数どうしを先に足すなど、計算の順序を工夫すると吸収の影響を減らせます。
4.2. 桁落ち
桁落ちとは何か
桁落ちは、近い値同士の引き算で有効桁数が大幅に減少する現象です。浮動小数点演算では特に深刻な問題となります。
近い数値を引くと上位の桁が打ち消され、小さな差が残ります。もとの数に含まれていた近似誤差が、その小さな差に対して大きな割合を占めることがあります。これが問題になる桁落ちです。近い数の引き算がいつも不正確になるわけではありません。
10進法の有効数字4桁で見てみます。\(1.235-1.234=0.001\) では、答えの有効数字は1桁しか残りません(図 13の右)。もし 1.235 と 1.234 が、それぞれ4桁目を四捨五入した値だったなら、本当の差は0.000〜0.002のどこかで、答えの0.001は1桁目からすでに信頼できません。
実例: 近い値の引き算
julia> a = 1.0000000000001
1.0000000000001
julia> b = 1.0
1.0
julia> a - b
9.992007221626409e-14 # 理論値は1.0e-13
a と b は非常に近い値です。理論的には次のようになるはずです。
ここでは、a を2進浮動小数点数として格納する時点で近似誤差が入っています。a - b の引き算自体は、格納された2数に対して正確です。それでも、もとの10進数どうしの差と比べると約0.08%の誤差があります。引き算が新たに誤差を生んだのではなく、入力時の小さな誤差が、差に対しては大きく現れたのです。
二次方程式の解の公式と桁落ち
桁落ちが実用上の問題を引き起こす典型例として、二次方程式の解の公式があります。
の解の公式は次式のとおりであることを思い出してください。
次の二次方程式を考えてみます。
これを展開すると
となります。この方程式の厳密な解は \(x_1 = 10000000\) と \(x_2 = 0.0000001\) です。
解の公式を使ってコンピューターで計算すると、次のようになります。
import math
a = 1
b = -10000000.0000001
c = 1
# 判別式
d = b*b - 4*a*c
# 解の公式
x1 = (-b + math.sqrt(d)) / (2*a)
x2 = (-b - math.sqrt(d)) / (2*a)
print(f"x1 = {x1}") # 出力: x1 = 10000000.0
print(f"x2 = {x2}") # 出力: x2 = 9.96515154838562e-08
\(x_1\) の値は正確ですが、\(x_2\) の真の値は \(1 \times 10^{-7}\) であるのに対し、計算結果は \(9.96515154838562 \times 10^{-8}\) となり、約0.35%の誤差が生じています。厳密な解に対して計算値は小さく出ており、この程度でも科学計算や金融計算では問題になることがあります。
なぜ桁落ちが起こるのか
\(x_2\) の計算では次のようになります。
この計算では、非常に大きな値 \(10000000.0000001\) とそれにごく近い値 \(\sqrt{(10000000.0000001)^2 - 4}\) の差を求めています。これらの差は本来非常に小さいですが、大きな数の引き算を経由するため、有効桁数が大幅に失われます。浮動小数点演算では、このような状況で精度の悪化が著しくなります。
対策と改善効果
解と係数の関係式を利用することで桁落ちを回避できます。二次方程式の解の積は \(\dfrac{c}{a}\) であるため、この例では \(x_1 \times x_2 = \dfrac{1}{1} = 1\) が成り立ちます。
そこで、以下のように計算手順を変更します。この実習用の例では、有限の係数を使い、判別式などの途中計算がオーバーフロー・アンダーフローしない場合を考えます。
import math
def solve_quadratic(a, b, c):
"""二次方程式を桁落ちを避けて解く"""
if a == 0:
raise ValueError("a は0以外とする")
d = b*b - 4*a*c
if d < 0:
raise ValueError("この例では実数解だけを扱う")
if b == 0 and c == 0:
return 0.0, 0.0
if b >= 0:
x1 = (-b - math.sqrt(d)) / (2*a)
else:
x1 = (-b + math.sqrt(d)) / (2*a)
# 桁落ちが少ない方の解を先に計算し、積の公式で他方を求める
x2 = c / (a * x1)
return x1, x2
a, b, c = 1, -10000000.0000001, 1
x1, x2 = solve_quadratic(a, b, c)
print(f"改良版解法: x1 = {x1}, x2 = {x2}")
# 出力例: 改良版解法: x1 = 10000000.0, x2 = 1e-07
従来解法と改良解法の比較
両方の解法で得られた結果を比較してみましょう。
| 解法 | x₁ | x₂ | x₂の相対誤差 |
|---|---|---|---|
公式通りの解法 |
10000000.0 |
9.96515154838562e-08 |
約0.35% |
改良解法 |
10000000.0 |
1e-07 |
約 \(4.5\times10^{-15}\) % |
従来解法では \(x_2\) に約0.35%の誤差が生じていましたが、改良解法では誤差が大幅に小さくなっています。ただし、表示が 1e-07 でも内部値は \(10^{-7}\) の近似値であり、誤差が0になったわけではありません。
4.3. コンピューターの計算に由来する実際の問題例
宇宙探査に円周率は何桁必要?
NASA・JPLの技術者Marc Raymanは、惑星間航行の計算には円周率として 3.141592653589793 を使うと説明しています。たとえば直径640億kmの円の周長を、この小数点以下15桁に丸めた円周率で計算しても、円周率の丸めに由来する差は約1.5cmです。
ここでの教訓は、桁数を際限なく増やすことではなく、必要な精度に対して、どの誤差がどれだけ影響するかを見積もることです。この例は円周率の近似だけの影響を見たもので、実際の航行計算全体の誤差が1.5cmという意味ではありません。
2038年問題(Year 2038 Problem)
多くのコンピューターシステムで時間を記録するために使われる「UNIXタイムスタンプ」は、1970年1月1日00:00:00 UTCを基準とする秒数です(POSIXではうるう秒を数えません)。これを32ビット符号付き整数で保持する実装では、2038年1月19日03:14:07 UTCに最大値(231-1 = 2,147,483,647)に達し、その次の1秒を表せなくなります。すでに64ビットなどの広い型を使う実装は、この32ビットの限界には該当しません。
桁あふれにより、時間が1901年12月13日20:45:52に「ジャンプバック」する可能性があり、時間に依存するシステムで重大な障害が発生する恐れがあります。この問題を避けるためには、64ビットタイムスタンプへの移行が必要です。
5. 練習問題
基本の確認から始め、計算の練習、考える問題の順に並べています。解答は、自分で解いてから開いてください。
4ビットで整数を表すとする。次の問に答えよ。
-
\(-3\) を符号付き絶対値表現で表せ。
-
\(-3\) を2の補数表現で表せ。
-
0を表すビット列は、符号付き絶対値表現と2の補数表現で、それぞれ何通りあるか。
解答
-
1011。符号ビットを1とし、残りの3ビットに絶対値3(011)を書きます。 -
1101。3は0011、各ビットを反転して1100、1を足して1101です。\(16-3=13=1101_{(2)}\) としても求められます。 -
符号付き絶対値表現では
0000(+0)と1000(−0)の2通り、2の補数表現では0000の1通りです。
(a)と(b)は、どちらも先頭のビットが1で「負」を表しますが、残りのビットの意味が違います。符号付き絶対値表現の 1101 は −5 を表すので、2つの表現を混同しないようにしましょう。
8ビットの2の補数表現で整数を表すとする。次の問に答えよ。
-
\(58_{(10)}\) を2進法で表せ。
-
\(-100_{(10)}\) を2の補数表現で表せ。
-
(a)と(b)で得られた2の補数表現を加算して \(58+(-100)\) を計算せよ。
-
(c)で得られた値の2の補数を計算して、対応する正の値(絶対値)を求め、10進法の表現に変換せよ。
解答
-
\(58 = 2^5 + 2^4 + 2^3 + 2^1\)、8ビットで
00111010です。 -
\(100 = 2^6 + 2^5 + 2^2 = 01100100_{(2)}\)。各ビットを反転して
10011011、1を足して10011100。よって \(-100 = 10011100_{\text{(2c)}}\) です。 -
次の筆算になります。9ビット目への桁上げは生じません。
\[\begin{array}{rrr} & 00111010_{\text{(2c)}} & 58\\ + & 10011100_{\text{(2c)}} & -100\\ \hline & 11010110_{\text{(2c)}} & \end{array}\] -
最上位ビットが1なので負の数です。
11010110の各ビットを反転して00101001、1を足して \(00101010_{(2)}=42\)。よって \(11010110_{\text{(2c)}}=-42\) で、\(58-100=-42\) と一致します。
異符号の2数の足し算なので、桁あふれは起こりません。(d)で、反転の前に1を引いたり、反転だけで止めたりする誤りが多いので注意しましょう。
次のビット列を8ビットの2の補数表現として読んだとき、表す整数を10進法で答えよ。
-
11110110 -
10000000 -
01111111 -
16進法で
FFと書かれた8ビットのビット列
解答
-
\(-10\)。最上位ビットが1なので負の数です。反転して
00001001、1を足して \(00001010_{(2)}=10\) なので −10 です。符号なしとして読んだ値246から容量256を引いて \(246-256=-10\) と求めることもできます。 -
\(-128\)。反転して1を足しても
10000000のままです。符号なしとして読むと128なので \(128-256=-128\) です。8ビットの2の補数表現で表せる最小の数で、+128 は表せません。 -
\(127\)。最上位ビットが0なので、そのまま符号なしとして読みます。8ビットで表せる最大の数です。
-
\(-1\)。\(\mathrm{FF}_{(16)}=11111111_{(2)}\) で、符号なしとして読むと255なので \(255-256=-1\) です。
同じビット列でも、符号なし整数として読むか、2の補数表現として読むかで値が変わります。ビット列だけからは、どちらの読み方をするのかは決まりません。
4ビットの2の補数表現で整数を表すとする。次の加算で桁あふれが発生するか否かを答えよ。発生する場合は、その理由を述べよ。
-
\(5+3\)
-
\(5+(-3)\)
-
\((-5)+(-4)\)
-
\((-5)+4\)
-
8ビットの2の補数表現の場合、\(100+100\) の計算結果はどのような値になるか。2の補数表現の筆算で確かめよ。
解答
-
発生する。正しい答え8は、4ビットの2の補数表現で表せる範囲 \(-8\sim 7\) を超えます。実際に
0101 + 0011 = 1000となり、−8 と読んでしまいます。5ビット目への桁上げは起きていないのに、桁あふれです。 -
発生しない。異符号の加算の答えは、2数の間に収まるからです。
0101 + 1101 = 1 0010で、5ビット目への桁上げを除いた0010が正しい答え2です。 -
発生する。正しい答え −9 は範囲 \(-8\sim 7\) を超えます。
1011 + 1100 = 1 0111の下位4ビット0111を読むと 7 になってしまいます。 -
発生しない。異符号の加算です。
1011 + 0100 = 1111で、正しい答え −1 です。 -
\(100=01100100_{(2)}\) より、以下の筆算で加算します。
\[\begin{array}{rrr} & 01100100_{\text{(2c)}} & 100\\ + & 01100100_{\text{(2c)}} & 100\\ \hline & 11001000_{\text{(2c)}} & \end{array}\]11001000は最上位ビットが1なので負の数として読まれます。反転して1を足すと \(00111000_{(2)}=56\) なので、計算結果は \(-56\) です(\(200-256=-56\) と同じ)。正しい答え200が範囲 \(-128\sim 127\) を超えるので、桁あふれです。
(a)と(c)のように、同符号の2数を足して結果の符号が変わったら桁あふれです。桁上げの有無では判定できないことを、(a)と(b)で確かめましょう。
10進法の \(-0.203125\) を16ビットの浮動小数点表現(半精度)で表すとき、次の問に答えよ。
-
\(-0.203125_{(10)}\) を2進法で表せ。
-
符号部を2進法で示せ。
-
指数部を2進法で示せ。
-
仮数部を2進法で示せ。
-
16ビット全体を16進法4桁で示せ。
解答
-
\(-0.203125_{(10)}=-0.001101_{(2)}\)。絶対値の 0.203125 に2を掛けながら、1の位に出てくる0または1を上から順に小数第一位から並べます。
\[\begin{array}{lll} 0.203125 & \times 2 =& \mathbf 0.40625\\ 0.40625 & \times 2 =& \mathbf 0.8125\\ 0.8125 & \times 2 =& \mathbf 1.625\\ 0.625 & \times 2 =& \mathbf 1.25\\ 0.25 & \times 2 =& \mathbf 0.5\\ 0.5 & \times 2 =& \mathbf 1. \end{array}\] -
負なので
1。 -
\(-0.001101_{(2)}=-1.101_{(2)}\times 2^{-3}\) より指数は −3 なので、\(-3+15=12=01100_{(2)}\)。
-
正規化した仮数 \(1.101_{(2)}\) から整数部の1を除くと
101。10ビットにするため右側に0を7つ補って1010000000。 -
1 01100 1010000000を4ビットずつ区切ると1011 0010 1000 0000なので、B280です。
| 符号(1ビット) | 指数(5ビット) | 仮数(10ビット) |
|---|---|---|
|
|
|
符号は仮数部ではなく符号部だけで表すので、仮数部に「負」の情報は入りません。指数部に −3 の2の補数表現を書いてしまう誤りにも注意しましょう(15を足すバイアス表現です)。(e)の区切りは、符号部・指数部・仮数部の境目とは関係なく、先頭から4ビットずつです。
-
半精度のビット列
0 10010 1010000000が表す値を10進法で答えよ。 -
次の10進数のうち、2進法の浮動小数点数で(仮数部の桁数が足りる限り)正確に表せるものをすべて選び、理由を説明せよ。
0.75、0.2、0.375、0.3、2.5
解答
-
13。符号部0で正、指数部 \(10010_{(2)}=18\) から15を引いて指数は3、仮数は省略した1を補って \(1.101_{(2)}\) です。\(1.101_{(2)}\times 2^{3}=1101_{(2)}=13\) となります。
-
0.75、0.375、2.5。\(0.75=\frac{3}{4}=0.11_{(2)}\)、\(0.375=\frac{3}{8}=0.011_{(2)}\)、\(2.5=\frac{5}{2}=10.1_{(2)}\) と、分母が2のべき乗の分数で書けるので、有限桁の2進小数になります。0.2 と 0.3 は既約分数 \(\frac{1}{5}\)、\(\frac{3}{10}\) の分母に因数5が含まれるので、2進法では循環小数になり、正確には表せません。
(b)は「10進法で桁が少ないか」ではなく「分母が2のべき乗か」で決まります。0.2 のように短い10進小数でも表現誤差が生じます。
10進法で有効桁数6桁の浮動小数点表現を用いて計算するものとする(各値は \(d.ddddd \times 10^n\) の形で表す)。次の問に答えよ。
-
\(a = 3.14159\), \(b = 3.14156\) とする。\(a - b\) を計算せよ。
-
(a)の結果を、有効桁数6桁の浮動小数点表現で表せ。
-
\(a,b\) が厳密な値なら、(a)の引き算に誤差はあるか。次に、\(a,b\) が未知の値を小数点以下5桁に丸めた近似値で、それぞれの絶対誤差が最大 \(0.000005\) である場合、差の絶対誤差は最大いくらになるか。
解答
-
\(3.14159 - 3.14156 = 0.00003\)
-
\(3.00000 \times 10^{-5}\)
-
入力が厳密なら、差 \(0.00003\) も正確に表せるので誤差はありません。入力が近似値なら、差の絶対誤差は最大 \(0.000005+0.000005=0.00001\) です。これは計算した差 \(0.00003\) に対して約33%にもなります。
(b)の表示は6桁ありますが、入力が近似値なら、下の5桁の0には意味がありません。表示の桁数だけから、答えの信頼できる桁数は決められないのです。
二次方程式 \(x^2 + 100x + 1 = 0\) の解を、10進法有効桁数6桁の浮動小数点表現で計算したい。\(\sqrt{9996} \fallingdotseq 99.9800\) (有効桁数6桁) とする。
-
解の公式から、\(x_1 = \dfrac{-100 - \sqrt{9996}}{2}\) を計算せよ。
-
解の公式から、\(x_2 = \dfrac{-100 + \sqrt{9996}}{2}\) を計算せよ。
-
二次方程式の解と係数の関係 \(x_1 \cdot x_2 = \dfrac{c}{a}\) を用いて、(a)の \(x_1\) から \(x_2\) を計算せよ。
-
(b)と(c)で得られた \(x_2\) の値を比較せよ。どちらが桁落ちの影響を受けているか考察せよ。
解答
-
\(x_1 = \dfrac{-100 - 99.9800}{2} = \dfrac{-199.980}{2} = -99.9900\)
-
\(x_2 = \dfrac{-100 + 99.9800}{2} = \dfrac{-0.0200}{2} = -0.0100\)。近い値どうしの引き算で、有効桁数が大幅に失われています。
-
\(c/a=1\) なので \(x_2 = \dfrac{1}{x_1} = \dfrac{1}{-99.9900} = -0.0100010\cdots \fallingdotseq -0.0100010\)
-
(b)は桁落ちの影響を受けています。(c)は引き算を経由しないため、桁落ちを避けられています。
より精密に計算した真の値は \(x_2=-0.01000100020\cdots\) で、(c)は有効数字6桁まで一致します。(b)は 100 と 99.9800 の上位の桁が打ち消し合い、6桁で計算しているのに差 0.0200 の有効数字は3桁しか残っていません。真の値との相対誤差は、(c)が約 \(2\times 10^{-8}\) なのに対し、(b)は約 \(1\times 10^{-4}\) と、およそ5000倍になります。本文の「対策と改善効果」と同じ考え方です。