サンプル・プログラム
大きな整数
では、現代の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>
浮動小数点数
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 となる。
整数の計算誤差
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>
BigInt型を使えば、このような誤差を起こすことなく計算できる。
JavaScriptでは計算誤差が出てもエラーを発生しない。このため、大きな数を扱うときは、整数なら BigInt型 を使い、小数なら事前にバリデーションを行う必要がある。
ブラウザによる違い
"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>
手元のブラウザを使った計算結果を掲げる。結果が異なるのは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
小数の計算誤差
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なぜ、小数第1位で計算誤差が出てしまうのか。これは、IEEE 754 浮動小数点数の計算方法に起因する。
b = 5.4 ... number
a - b = 0.5999999999999996 ... number
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>
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>
これは、整数化する100を乗ずる計算で循環小数になってしまうためだ。
小数を文字列として扱い、小数点を消して整数演算を行い、最後に除算して小数に戻すという方法もあるが、かなり複雑になる。
小数が登場するのは何らかの計測データだろうから、あらかじめ有効数字を決めておき、計算結果を丸めるというやり方も考えられる。
これらの計算誤差は、PHPやPythonでも発生する。C++(g++)では発生しなかった。詳しくは「有限小数、循環小数、分数、無理数」をご覧いただきたい。
オーバーフロー
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>
計算オーバーフローが起き、変数bには "Infinity" という文字列が代入される。
BigInt型 では正常に計算ができる。
a = 1.7976931348623157e+308...number大きな数を扱う場合は、Infinity 対策をしておかないとプログラムが予期しない動きをする可能性がある。
b = Infinity...number
c = 359538626972463141629054847463408713596141135051689993197834953606314521560057077521179117265533756343080917907028764928468642653778928365536935093407075033972099821153102564152490980180778657888151737016910267884609166473806445896331617118664246696549595652408289446337476354361838599762500808052368249716736...bigint
コラム:分数計算
Pythonでは、小数の計算誤差が起きると書いたが、標準ライブラリfractionを使って分数計算ができるので、計算誤差の回避は可能である。それ以外のプログラミング言語では、自力で関数やクラスを用意する必要があるだろう。
コラム:BigInt型
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 = 0n; i <= k; i++) {
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: }
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 = 0; k <= 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: }
素数判定には、「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; //判定できる整数の最大桁数
JavaScript ES6 の仕様に BigInt型 の最大値の制限はないのだが、ミラー・ラビン素数判定は比較的高速な素数判定アルゴリズムとはいえ、桁数が増えると幾何級数的に計算量が急増する。一般的なパソコンの計算能力では、せいぜい300~400桁が我慢できる待ち時間の限界だろう。
また、コラム欄では、Number.MAX_VALUE を超える整数だけを扱うことができる BigInt型 を使って、巨大素数を生成・判定するプログラムを紹介する。