2.4 数値型の範囲と誤差

(1/1)
金髪ツインテールの女性が数値型の範囲と誤差を学ぶ
数値型の範囲と誤差を学ぶ
JavaScript のNumber型は、およそ±1.8×10308までの有限値を表せる。0に最も近い正の値は約4.94×10-324である。ただし、2進数の有限桁で値を表すため、多くの10進小数は近似値となり、簡単な計算でも丸め誤差が生じる。また、Number.MAX_VALUE を超える計算ではInfinityになるため、対策が必要だ。

また、コラム欄では、任意精度の整数を扱う BigInt型 を使って、巨大な素数候補を生成・判定するプログラムを紹介する。BigIntは整数専用であり、小数は扱えない。

目次

サンプル・プログラム

大きな整数

前回、プログラム電卓用に開発されたインテル 4004 は4ビットCPUだったと書いた。4ビットの並びは $2^4=16$ 通りあり、0~15の符号なし整数を表せる。ただし、CPUのビット数だけで計算できる数の範囲が決まるわけではなく、複数の桁に分けて計算する命令やプログラムも使える。
1972年(昭和47年)、インテルは8ビットCPU 8008 を発表した。その後、汎用レジスタやアドレスの基本幅は16ビット、32ビット、64ビットへ広がった。64ビットの並びは $2^{64}=18,446,744,073,709,551,616$ 通りあるが、符号の有無、命令、データ型によって表せる範囲は異なる。CPUのビット数とJavaScriptのNumber型の精度は、同じ意味ではない。

では、プログラムで扱える整数の大きさは、CPUのビット数に左右されるのかというと、そうであるときと、そうでないときがある。
JavaScriptの場合、Number.MAX_SAFE_INTEGER は、Number型で正確に区別できる「安全な整数」の最大値を表す。処理系ごとに変わる値でも、Number型で表せる最大の整数でもない。

minmax1.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     document.getElementById('var1').innerHTML = Number.MAX_SAFE_INTEGER;
  23: };
  24: </script>

"minmax1.html" は Number.MAX_SAFE_INTEGER を表示するプログラムだ。実行環境にかかわらず、ECMAScript仕様で定められた 9007199254740991($2^{53}-1$)になる。64ビットCPUの符号なし整数の最大値とは異なる。

浮動小数点数

これまで整数の話ばかりしてきたが、数値計算では小数も扱わなければならない。JavaScriptの Number型は、整数と小数を区別せず、基本的に同じ数値型として扱う。
電気・電子分野の標準化団体 IEEE (アイ・トリプル・イー)  が定めた IEEE 754 規格には、コンピュータで浮動小数点数を扱う方式が規定されている。JavaScriptのNumber型は、そのbinary64(倍精度)に対応する。
浮動小数点数
浮動小数点数
IEEE 754 浮動小数点数
IEEE 754 浮動小数点数
IEEE 754のbinary64 は、符号部1ビット、指数部11ビット、仮数を表す小数部52ビットの合計64ビットで構成される。正規化された有限値では先頭の1が省略されるため、有効桁は53ビット分ある。このように指数によって小数点の位置を動かして表す数を浮動小数点数と呼ぶ。

JavaScriptで安全に扱える整数の最大値 Number.MAX_SAFE_INTEGER は $2^{53}-1=9,007,199,254,740,991$ である。これを超える整数もNumber型で表せるが、隣り合う整数を区別できなくなる場合がある。
安全な整数の最小値は Number.MIN_SAFE_INTEGER で定義され、$-(2^{53}-1)=-9,007,199,254,740,991$ となる。

Number型で表せる有限値の最大絶対値は約 1.7976931348623157×10308 である。最小の正の正規化数は約2.225×10-308だが、非正規化数を含めると0より大きい最小値は Number.MIN_VALUE、約4.94×10-324である。

整数の計算誤差

"minmax2.html" は Number.MAX_SAFE_INTEGER と、それに1を加算した結果、8を加算した結果を表示するプログラムだ。

minmax2.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     let a = Number.MAX_SAFE_INTEGER;
  23:     document.getElementById('let1').innerHTML = 'a = ' + a.toLocaleString() + '...' + typeof a;
  24: 
  25:     //Numberの最大値+1
  26:     let b = a + 1;
  27:     document.getElementById('let2').innerHTML = 'b = ' + b.toLocaleString() + '...' + typeof b;
  28: 
  29:     //Numberの最大値+8
  30:     let c = a + 8;
  31:     document.getElementById('let3').innerHTML = 'c = ' + c.toLocaleString() + '...' + typeof c;
  32: 
  33:     //BigIntがあれば
  34:     if (typeof BigInt !'undefined') {
  35:         let d = BigInt(a+ BigInt(8);
  36:         document.getElementById('let4').innerHTML = 'd = ' + d.toLocaleString() + '...' + typeof d;
  37:     }
  38: };
  39: </script>

1を加えた値までは正確に表せるが、その先では表せる整数の間隔が2以上になるため、8を加えた結果は最も近い表現可能な値へ丸められる。
BigInt型を使えば、このような誤差を起こすことなく計算できる。

JavaScriptは、安全な整数の範囲を外れても自動的にはエラーを出さない。正確な整数が必要なら Number.isSafeInteger で確認するか BigInt型 を使う。小数では、用途に応じて許容誤差、丸め規則、有効桁を決め、金額など10進数での正確さが必要な処理では整数化や10進演算ライブラリを検討する。

古いブラウザとの違い

Internet Explorer 11には Number.MAX_SAFE_INTEGER とBigIntが実装されていない。"minmax3.html" は、MAX_SAFE_INTEGERがない場合に $2^{53}-1$ を計算する歴史的な互換例である。IE11デスクトップアプリは一部のWindows 10で2022年(令和4年)6月15日にサポートを終了しており、新しいプログラムでは標準的な現行ブラウザを対象にする。

minmax3.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     console.log(Number.MAX_SAFE_INTEGER);
  23:     let a;
  24:     if (typeof Number.MAX_SAFE_INTEGER !'undefined') {
  25:         a = Number.MAX_SAFE_INTEGER;
  26:     } else {
  27:         a = Math.pow(2, 53- 1;
  28:     }
  29:     document.getElementById('let1').innerHTML = 'a = ' + a + '...' + typeof a;
  30: 
  31:     //Numberの最大値+1
  32:     let b = a + 1;
  33:     document.getElementById('let2').innerHTML = 'b = ' + b + '...' + typeof b;
  34: 
  35:     //Numberの最大値+8
  36:     let c = a + 8;
  37:     document.getElementById('let3').innerHTML = 'c = ' + c + '...' + typeof c;
  38: 
  39:     //BigIntがあれば
  40:     if (typeof BigInt !'undefined') {
  41:         let d = BigInt(a+ BigInt(8);
  42:         document.getElementById('let4').innerHTML = 'd = ' + d + '...' + typeof d;
  43:     }
  44: };
  45: </script>

IE11でもNumber型の計算自体はIEEE 754の倍精度浮動小数点数に基づく。

2021年(令和3年)当時の実行結果を参考として掲げる。IE11ではBigIntの行が表示されないが、Number型の3行は同じ結果である。
●IE11(Windows 10 64bit)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
●Edge 91.0.864.64(Windows 10 64bit)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
d = 9007199254740999...bigint
●Chrome 91.0.4472.114(Windows 10 64bit)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
d = 9007199254740999...bigint
●Chrome 91.0.4472.120(Android)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
d = 9007199254740999...bigint
●Firefox 89.0.2(Windows 10 64bit)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
d = 9007199254740999...bigint
●Safari(iOS 14.6)
a = 9007199254740991...number
b = 9007199254740992...number
c = 9007199254741000...number
d = 9007199254740999...bigint

小数の計算誤差

"calcError1.html" は $ 6 - 5.4 $ という簡単な小数の減算結果を表示するものだ。

calcError1.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     let a = 6;
  23:     document.getElementById('let1').innerHTML = 'a = ' + a.toString() + ' ... ' + typeof a;
  24: 
  25:     let b = 5.4;
  26:     document.getElementById('let2').innerHTML = 'b = ' + b.toString() + ' ... ' + typeof b;
  27: 
  28:     let c = a - b;
  29:     document.getElementById('let3').innerHTML = 'a - b = ' + c.toString() + ' ... ' + typeof c;
  30: };
  31: </script>

だが、次のような残念な計算結果になる。
a = 6 ... number
b = 5.4 ... number
a - b = 0.5999999999999996 ... number
なぜ、小数第1位で計算誤差が出てしまうのか。これは、IEEE 754 浮動小数点数の計算方法に起因する。

1.0未満の小数部は、分母が2のべき乗となる分数、すなわち $ \displaystyle \frac{1}{2} , \frac{1}{4} , \frac{1}{8} $...の組み合わせで表現しなければならない。
ところが $ 0.4 = 2^{-2} + 2^{-3} + 2^{-6} + 2^{-7} \cdots $ のように循環小数になってしまうのだ。
このため、簡単な減算であるにも関わらず、誤差が発生してしまった。

calcError2.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     const mag = 10;     //倍率
  23: 
  24:     let a = 6;
  25:     document.getElementById('let1').innerHTML = 'a = ' + a.toString() + ' ... ' + typeof a;
  26: 
  27:     let b = 5.4;
  28:     document.getElementById('let2').innerHTML = 'b = ' + b.toString() + ' ... ' + typeof b;
  29: 
  30:     let c = (a * mag - b * mag) / mag;
  31:     document.getElementById('let3').innerHTML = 'a - b = ' + c.toString() + ' ... ' + typeof c;
  32: };
  33: </script>

そこで、倍率定数 mag を設け、計算前に10倍して整数として減算し、最後に mag で除算すると、この例では0.6を得られる。ただし、どの小数でも正確になる方法ではない。

calcError3.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     const mag = 100;        //倍率
  23: 
  24:     let a = 20.42;
  25:     document.getElementById('let1').innerHTML = 'a = ' + a.toString() + ' ... ' + typeof a;
  26: 
  27:     let b = 10.42;
  28:     document.getElementById('let2').innerHTML = 'b = ' + b.toString() + ' ... ' + typeof b;
  29: 
  30:     let c = (a * mag - b * mag) / mag;
  31:     document.getElementById('let3').innerHTML = 'a - b = ' + c.toString() + ' ... ' + typeof c;
  32: };
  33: </script>

ところが、$ 20.42 - 10.42 $ では、整数化しても計算誤差が発生してしまう。
これは、整数化する100を乗ずる計算で循環小数になってしまうためだ。

金額など小数桁数が決まっている値は、入力を文字列として読み、最小単位の整数へ変換して計算する方法がある。測定値では有効数字と丸め規則を決め、比較するときは許容誤差を考慮する。一般的な10進演算が必要なら、検証された10進演算ライブラリを使う方が安全である。

同じIEEE 754の倍精度を使うPHP、Python、C++などでも、内部では同種の表現誤差が起こり得る。画面表示の桁数や丸め方によって、誤差が見えない場合がある。詳しくは「有限小数、循環小数、分数、無理数」をご覧いただきたい。

オーバーフロー

JavaScriptでは扱える最大の数値は Number.MAX_VALUE という定数に代入されている。

minmax4.html

  19: <script>
  20: //ページのロード時に実行
  21: window.onload = function() {
  22:     const a = Number.MAX_VALUE;
  23:     document.getElementById('let1').innerHTML = 'a = ' + a + '...' + typeof a;
  24: 
  25:     //Numberの最大値×1.1
  26:     let b = a * 2;
  27:     document.getElementById('let2').innerHTML = 'b = ' + b + '...' + typeof b;
  28: 
  29:     //BigIntで2の1024乗を正確に計算
  30:     if (typeof BigInt !'undefined') {
  31:         const c = 2n ** 1024n;
  32:         document.getElementById('let3').innerHTML = 'c = ' + c + '...' + typeof c;
  33:     }
  34: };
  35: </script>

"minmax4.html" は Number.MAX_VALUE を2倍するものだ。
計算オーバーフローが起き、変数bは文字列ではなくNumber型の特殊値 Infinity になる。
BigInt型 では $2^{1024}$ を正確な整数として計算できるが、Number型とBigInt型は用途が異なり、演算前に型をそろえる必要がある。
a = 1.7976931348623157e+308...number
b = Infinity...number
c = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216...bigint
大きな数を扱う場合は、Infinity 対策をしておかないとプログラムが予期しない動きをする可能性がある。

コラム:分数計算

金髪ツインテールの女性が夜に分数を勉強する
分数を使った正確な計算を学ぶ
分子と分母を整数として保持する分数計算なら、有理数を正確に表せる。ただし、分母が0の値や、円周率のような無理数は別に扱う必要がある。JavaScriptの標準組み込み型には分数型がないため、ライブラリを使うか、BigIntで分子と分母を管理するクラスなどを用意する。
Pythonには標準ライブラリの fractions があり、Fraction型で有理数を計算できる。10進小数を正確に扱いたい場合は、言語やライブラリが提供する10進型も選択肢になる。
小学校の算数で、分数の計算でつまずく子どもが多いと聞くが、プログラミングでは分数の考え方が必要になる場面がある。前回のコラムで、プログラミングを学ぶ前に算数の基礎力を付けた方がいいと書いたが、分数についても同じことが言える。

コラム:BigInt型

サンプル・プログラムの実行例
サンプル・プログラムの実行例
JavaScriptBigInt型 は、巨大な整数のほか、連番や時刻などNumber型の安全な整数範囲を超える値にも使える。ここではBigIntの例として、巨大な素数候補を生成・判定するプログラムを紹介する。素数は暗号技術でも使われるが、このサンプルは学習用であり、暗号鍵の生成には使用しない。
BigInt型 の定数は、10進整数の末尾に $ n $ を付記し、 "1234567890n" のように表記する。
BigInt型 では多くの算術演算子、ビット演算子、比較演算子を使える。ただし、Number型と混ぜた算術演算はできず、Mathオブジェクトの多くの関数もBigIntを受け付けない。乱数、べき剰余、素数判定はユーザー関数として用意する。

bigPrime.html

  91: /**
  92:  * BigIntの乱数を求める.
  93:  * @param   BigInt min 最小値
  94:  * @param   BigInt max 最大値
  95:  * @return  BigInt min以上max以下の乱数
  96:  * @throws  RangeError minがmaxより大きい
  97: */
  98: function BigIntRandom(min, max) {
  99:     min = BigInt(min);
 100:     max = BigInt(max);
 101:     if (min > max) {
 102:         throw new RangeError('minはmax以下にしてください');
 103:     }
 104: 
 105:     const range = max - min + 1n;
 106:     const bitLength = range.toString(2).length;
 107:     const byteLength = Math.ceil(bitLength / 8);
 108:     const excessBits = byteLength * 8 - bitLength;
 109:     const bytes = new Uint8Array(byteLength);
 110:     let value;
 111:     do {
 112:         crypto.getRandomValues(bytes);
 113:         if (excessBits > 0) {
 114:             bytes[0&0xff >>> excessBits;
 115:         }
 116:         value = 0n;
 117:         for (const byte of bytes) {
 118:             value = (value << 8n| BigInt(byte);
 119:         }
 120:     } while (value >range);
 121: 
 122:     return min + value;
 123: }

ユーザー関数 BigIntRandom は、指定範囲のBigInt乱数を生成する。Math.random は暗号学的に安全ではなく、丸め方による偏りも生じるため、このサンプルでは crypto.getRandomValues でランダムなバイト列を作り、範囲外の値を捨てる方法を用いる。

bigPrime.html

 159: /**
 160:  * ミラー・ラビン素数判定法により引数(BigInt)が素数かどうかを判定する.
 161:  * @param   BigInt n 判定する整数
 162:  * @return  boolean true:おそらく素数/false:合成数
 163: */
 164: function MillerRabinPrimalityTest(n, rounds = 20) {
 165:     if (n < 2n) {
 166:         return false;
 167:     }
 168:     if ((n == 2n|| (n == 3n)) {
 169:         return true;
 170:     }
 171:     if ((n & 1n) == 0n) {
 172:         return false;
 173:     }
 174: 
 175:     let d = n - 1n;
 176:     let s = 0;
 177:     while ((d & 1n) == 0n) {
 178:         d >>1n;
 179:         s++;
 180:     }
 181: 
 182:     for (let i = 0i < roundsi++) {
 183:         const a = BigIntRandom(2n, n - 2n);
 184:         let x = BigIntModpow(a, d, n);
 185:         if ((x == 1n|| (x == n - 1n)) {
 186:             continue;
 187:         }
 188: 
 189:         let witness = true;
 190:         for (let r = 1r < sr++) {
 191:             x = (x * x% n;
 192:             if (x == n - 1n) {
 193:                 witness = false;
 194:                 break;
 195:             }
 196:         }
 197:         if (witness) {
 198:             return false;
 199:         }
 200:     }
 201:     return true;
 202: }

ユーザー関数 MillerRabinPrimalityTest は、与えた BigInt型 の整数が素数かどうかを確率的に判定する。
素数判定には、「PHPとPythonで巨大素数を扱う」で紹介したミラー・ラビン素数判定を用いている。
ここで使う対数関数 BigIntLog、べき剰余関数 BigIntModpow は、前述の通り、ユーザー関数として用意した。

bigPrime.html

 125: /**
 126:  * BigIntの対数を求める.
 127:  * @param   BigInt n    整数
 128:  * @param   BigInt base 基数
 129:  * @return  BigInt 対数
 130: */
 131: function BigIntLog(n, base) {
 132:     let l = 0n;
 133:     let x = n;
 134:     while ((x = x / base> 0n) {
 135:         l++;
 136:     }
 137:     return l;
 138: }

bigPrime.html

 140: /**
 141:  * BigIntのべき剰余を求める.
 142:  * @param   BigInt base 基数
 143:  * @param   BigInt exp  指数
 144:  * @param   BigInt mod  除数
 145:  * @return  BigInt べき剰余
 146: */
 147: function BigIntModpow(base, exp, mod) {
 148:     let res = 1n;
 149:     while (exp > 0n) {
 150:         if ((exp & 1n) == 1n) {
 151:             res = (res * base% mod;
 152:         }
 153:         base = (base * base% mod;
 154:         exp >>1n;
 155:     }
 156:     return res;
 157: }

bigPrime.html

  32: const MAX_DIGITS  = 200;        //判定できる整数の最大桁数

定数 MAX_DIGITS に、生成・判定できる整数の最大桁数を代入しておく。
ECMAScript 仕様はBigIntの固定最大値を定めていないが、実際の上限はメモリや処理系に左右される。ミラー・ラビン素数判定は確率的判定なので、結果は「おそらく素数」であり、試行回数を増やすほど合成数を素数と誤判定する確率を下げられる。桁数が増えるほど生成・判定時間も長くなるため、このサンプルでは最大200桁に制限している。

コラム:表示と内部の値は同じとは限らない

金髪ツインテールの女性が円周率の丸めを講義する
円周率を小数第3位までに丸める
本文のサンプル・プログラム "calcError1.html" では、演算結果が0.599999‥‥と表示された。同じIEEE 754の値でも、プログラミング言語、処理系、出力関数、書式指定によって画面に出す桁数が異なるため、0.6と表示される場合もある。
短く表示されたからといって、内部の表現誤差が消えたわけではない。出力時は用途に合った有効数字と丸め方を明示し、計算時の精度とは分けて考える必要がある。
まず、JavaScriptだが、下記のスクリプトを実行してみてほしい。
let a = 0.599999999999999;
console.log(a);
通常は0.599999999999999と表示される。このリテラルは、その値へ変換したときに同じbinary64値へ戻せる短い10進表記として出力される。

次は、Pythonだ。
a = 0.599999999999999
print(a);
print関数 でも通常は0.599999999999999と表示される。ただし、Pythonのfloatにも2進浮動小数点数の表現誤差はある。

それでは、PHPはどうか――
$a = 0.599999999999999;
print($a);
出力結果はPHPのバージョンや precision 設定に左右される。0.6と表示されても、内部計算が10進数で正確になったことを意味しない。

最後に、C++はどうだろう。
double a = 0.599999999999999;
std::cout << a << std::endl;
既定の精度では0.6と表示されることがある。必要な桁数は std::setprecision などで指定する。C++のdoubleにも同じ種類の表現誤差があり、表示だけで精度を判断してはいけない。

参考サイト

(この項おわり)
header