2.4 数値型の範囲と誤差

(1/1)
電卓のイラスト(文房具)
JavaScript で扱える数値の範囲は±2.2×10-308~±1.8×10308の範囲だが、数値が2進数で表現する関係から、簡単な10進数小数計算でも誤差が発生する場合がある。また、この範囲を超えて(Number.MAX_VALUE を超えるような)計算する場合は、オーバーフロー対策が必要だ。

また、コラム欄では、Number.MAX_VALUE を超える整数だけを扱うことができる BigInt型 を使って、巨大素数を生成・判定するプログラムを紹介する。

目次

サンプル・プログラム

大きな整数

前回、プログラム電卓用に開発されたCPUのインテル 4004 は4ビットだったと書いた。\( 4bit = 2^4 = 16 \)種類の値を扱うことができるから、10進数1桁(10種類)の数を扱うのに十分だった。
では、現代のCPUも4ビットで、それを4コアや8コアにすることで多桁計算をしているかというと、そういうわけではない。1972年(昭和47年)4月、インテルは8ビットCPU 8008 を発表し、8ビットの加減算ができる命令を内蔵した。\( 8bit = 2^8 = 256 \)、つまり0~255の整数の加減算ができるようになったが、これでは計算機としては非力だろう。
その後、CPUは16ビット、32ビットと進化し、現在は64ビットになっている。つまり、\( 64bit = 2^{64} = 18,446,744,073,709,551,616 \)(0~1844京6744兆737億955万1615)の値を扱えるようになっている。計算機としては十分すぎる性能だ。

では、プログラムで扱える整数の大きさは、CPUのビット数に左右されるのかというと、そうであるときと、そうでないときがある。
JavaScriptの場合、Number.MAX_SAFE_INTEGER という定数に、その処理系で扱える最大の整数値が代入されている。

  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 を表示するだけのプログラムだ。Windows 10(64ビット)のGoogle Chromeで実行したところ 9007199254740991 (9007兆1992億5474万991)という結果になる。これは、64ビットの最大値1844京6744兆737億955万1615とは全く異なる値だ。一体どういうことなのだろう。

浮動小数点数

これまで整数の話ばかりしてきたが、数値計算では小数も扱わなければならない。C/C++やPythonなどのプログラミング言語と異なり、JavaScriptの Number型整数と小数の両方を扱うデータ型である。
IEEE (アイ・トリプル・イー) という国際会議において、コンピュータで小数を扱う方式が定義されている。JavaScriptは IEEE 754 規格(倍精度浮動小数点数)に則って小数を扱う。
浮動小数点数
浮動小数点数
IEEE 754 浮動小数点数
IEEE 754 浮動小数点数
IEEE 754 では、小数を符合部、仮数部、指数部の3つにわけ、符合部1ビット、仮数部52ビット、指数部11ビットの合計64ビットになる。このように符合部、仮数部、指数部に分けた小数を浮動小数点数と呼ぶ。

JavaScriptで扱える整数の最大値 Number.MAX_SAFE_INTEGER とは、符合部がプラスで、仮数部52ビット全部1で埋め、指数部が1――つまり \( 2^{53} - 1 = 9,007,199,254,740,991 \) のことである。
なお、整数の最小値は Number.MIN_SAFE_INTEGER で定義されており、同様の計算で \( -(2^{53} - 1) = -9,007,199,254,740,991 \) となる。

また、JavaScriptで扱える小数の範囲は ±2.2×10-308~±1.8×10308 となる。

整数の計算誤差

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

  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は正しく加算されるが、8を加算した結果はおかしなことになっている。これは計算の過程で、指数部が1つ加算され、仮数部からビットが抜け落ちたために起きている。
BigInt型を使えば、このような誤差を起こすことなく計算できる。

JavaScriptでは計算誤差が出てもエラーを発生しない。このため、大きな数を扱うときは、整数なら BigInt型 を使い、小数なら事前にバリデーションを行う必要がある。

ブラウザによる違い

マイクロソフトInternet Explorer(IE)を使っている方は、ここまでのプログラムが動かなかっただろう。申し訳ない。IEには Number.MAX_SAFE_INTEGER が定義されていないためだ。
"minmax2.html" を IE対応にしたプログラムが "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, 52- 1 + Math.pow(2, 52);
  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>

IEも IEEE 754 に則った計算をしていることが分かる。

手元のブラウザを使った計算結果を掲げる。結果が異なるのはIE11だけだ。
●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 \) という簡単な小数の減算結果を表示するものだ。

  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 \) のように循環小数になってしまうのだ。
このため、簡単な減算であるにも関わらず、誤差が発生してしまった。

  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 を設け、計算前に整数に変換し、最後に mag で除算することで正しい結果を求めることができる。

  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を乗ずる計算で循環小数になってしまうためだ。

小数を文字列として扱い、小数点を消して整数演算を行い、最後に除算して小数に戻すという方法もあるが、かなり複雑になる。
小数が登場するのは何らかの計測データだろうから、あらかじめ有効数字を決めておき、計算結果を丸めるというやり方も考えられる。

これらの計算誤差は、PHPやPythonでも発生する。C++(g++)では発生しなかった。詳しくは「有限小数、循環小数、分数、無理数」をご覧いただきたい。

オーバーフロー

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

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

"minmax4.htm" は Number.MAX_VALUE を2倍するものだ。
計算オーバーフローが起き、変数bには "Infinity" という文字列が代入される。
BigInt型 では正常に計算ができる。
a = 1.7976931348623157e+308...number
b = Infinity...number
c = 359538626972463141629054847463408713596141135051689993197834953606314521560057077521179117265533756343080917907028764928468642653778928365536935093407075033972099821153102564152490980180778657888151737016910267884609166473806445896331617118664246696549595652408289446337476354361838599762500808052368249716736...bigint
大きな数を扱う場合は、Infinity 対策をしておかないとプログラムが予期しない動きをする可能性がある。

コラム:分数計算

勉強のイラスト「テスト勉強・男の子」
浮動小数点数の仕組みを見て気づいた方がいると思うが、分数で計算すれば、本編で紹介したような計算誤差は起きない。しかし、JavaScriptには分数を計算する仕組みが備わっていない
Pythonでは、小数の計算誤差が起きると書いたが、標準ライブラリfractionを使って分数計算ができるので、計算誤差の回避は可能である。それ以外のプログラミング言語では、自力で関数やクラスを用意する必要があるだろう。
小学校の算数で、分数の計算でつまずく子どもが多いと聞くが、プログラミングでは分数の考え方が必要になる場面がある。前回のコラムで、プログラミングを学ぶ前に算数の基礎力を付けた方がいいと書いたが、分数についても同じことが言える。

コラム:BigInt型

サンプル・プログラムの実行例
サンプル・プログラムの実行例
JavaScriptBigInt型 を使うのは、暗号処理など限られた用途で、プログラミングだけでなく数学の素養も求められることから、本編では取り上げない。ここで1つだけ、巨大素数を BigInt型 を使って生成・判定するプログラムを紹介しておく。暗号処理と素数は密接な関係にあるが、この内容は初心者向けではないので読み飛ばしていただいて構わない。
BigInt型 の定数は、10進整数の末尾に \( n \) を付記し、 "1234567890n" のように表記する。
BigInt型 では、四則演算子やシフト演算子、論理演算子などは適用できるが、Math系の算術関数が適用できない。巨大素数を発生するのに使う乱数関数や、素数かどうかを判定するのに使う対数関数、べき剰余関数はユーザー関数として用意しなければならない。

  90: /**
  91:  * BigIntの乱数を求める.
  92:  * @param   BigInt min 最小値
  93:  * @param   BigInt max 最大値
  94:  * @return  BigInt 乱数/NaN:minとmaxが間違っている
  95: */
  96: function BigIntRandom(min, max) {
  97:     //引数のバリデーション
  98:     if (min >max) {
  99:         return NaN;
 100:     }
 101: 
 102:     let n, e;
 103:     let k = BigInt(String(max).length- 1n;
 104:     do {
 105:         n = 0n;
 106:         for (let i = 0ni <ki++) {
 107:             e = BigInt(Math.round(Math.random() * 10));
 108:             n +e * 10n ** i;
 109:         }
 110:     } while ((n < min|| (n > max));
 111: 
 112:     return n;
 113: }

ユーザー関数 BigIntRandom は、BigInt型 の整数乱数を発生させる。
BigInt型 の整数を文字列とみなし、その桁数(長さ)分の10進数文字(0~9)を Math.round 関数を使ってランダムに並べ、最後にBigInt型に変換するようにした。

 149: /**
 150:  * ミラー・ラビン素数判定法により引数(BigInt)が素数かどうかを判定する.
 151:  * @param   BigInt n 判定する整数
 152:  * @return  boolen true:素数である/false:素数でない
 153: */
 154: function MillerRabinPrimalityTest(n) {
 155:     //2は素数
 156:     if (n == 2n) {
 157:         return true;
 158:     }
 159:     //1または偶数は素数では内
 160:     if ((n == 1n|| ((n & 1n) == 0n)) {
 161:         return false;
 162:     }
 163: 
 164:     d = (n - 1n>> 1n;
 165:     while ((d & 1n) == 0n) {
 166:         d >>1n;
 167:     }
 168:     for (k = 0k <=  BigIntLog(n, 4n); k++) {
 169:         a = BigIntRandom(1n, n - 1n);
 170:         t = d;
 171:         y = BigIntModpow(a, t, n);
 172:         while ((t !n - 1n&& (y !1n&& (y !n - 1n)) {
 173:             y = (y * y% n;
 174:             t <<1n;
 175:         }
 176:         if ((y !n - 1n&& ((t & 1n) == 0n)) {
 177:             return false;
 178:         }
 179:     }
 180:     return true;
 181: }

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

 115: /**
 116:  * BigIntの対数を求める.
 117:  * @param   BigInt n    整数
 118:  * @param   BigInt base 基数
 119:  * @return  BigInt 対数
 120: */
 121: function BigIntLog(n, base) {
 122:     let l = 0n;
 123:     let x = n;
 124:     while ((x = x / base> 0n) {
 125:         l++;
 126:     }
 127:     return l;
 128: }

 130: /**
 131:  * BigIntのべき剰余を求める.
 132:  * @param   BigInt base 基数
 133:  * @param   BigInt exp  指数
 134:  * @param   BigInt mod  除数
 135:  * @return  BigInt べき剰余
 136: */
 137: function BigIntModpow(base, exp, mod) {
 138:     res = 1n;
 139:     while (exp > 0n) {
 140:         if ((exp & 1n) == 1n) {
 141:             res = (res * base% mod;
 142:         }
 143:         base = (base * base% mod;
 144:         exp >>1n;
 145:     }
 146:     return res;
 147: }

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

定数 MAX_DIGITS に、生成・判定できる整数の最大桁数を代入しておく。
JavaScript ES6 の仕様に BigInt型 の最大値の制限はないのだが、ミラー・ラビン素数判定は比較的高速な素数判定アルゴリズムとはいえ、桁数が増えると幾何級数的に計算量が急増する。一般的なパソコンの計算能力では、せいぜい300~400桁が我慢できる待ち時間の限界だろう。
(この項おわり)
header