JavaScript では、通常の数値 number
として安全に表せる整数の最大値として、Number.MAX_SAFE_INTEGER
が定義されている。
JavaScript の数値は浮動小数点数で表されるため、大きすぎる値の計算を行おうとすると下位が丸められてしまうことがある。
Number.MIN_SAFE_INTEGER
以上 Number.MAX_SAFE_INTEGER
以下の整数であれば、このような丸めの影響を避け、正確に表現・計算できるとされている。
しかし、本当にこの範囲内の整数はすべて正確に計算できるのだろうか?
特に、割り切れない割り算を行ったとき、丸めの影響が出て予期しない値にならないだろうか?
今回は、これを直感的に確認する。
この記事では、厳密な証明は行わない。
加算・減算・乗算
整数同士の加算・減算・乗算の結果は、必ず整数になる。
そのため、計算結果が Number.MIN_SAFE_INTEGER
以上 Number.MAX_SAFE_INTEGER
以下であれば、必ず正確に浮動小数点数で表すことができるので、正確な結果が得られると考えられる。
除算
浮動小数点数における負の数の表現は、負であることを表すビットを立て、そのほかの部分は変えずに表す。
よって、負の数がかかわる演算には対象性がありそうである。
そこで、今回は非負整数を正の整数で割ることだけを考える。
このとき、商は必ず割られる数以下になる。
1で割る場合
1で割る場合、商は割られる数と等しくなる。
よって、割られる数として表現できる値は、必ず商としても表現でき、正確な結果が得られると考えられる。
2で割る場合
「2で割る」操作は、仮数部を変えず、指数部を1減らすことで実現できる。
よって、浮動小数点数の商は必ず正確な値になり、適切な方法で丸めれば正確な整数の結果が得られると考えられる。
3で割る場合
Number.MAX_SAFE_INTEGER
付近の数をいくつか 3 で割ってみる。
for (let i = 10; i >= 0; i--) {
const value = Number.MAX_SAFE_INTEGER - i;
console.log(`${value} / 3 = ${value / 3}`);
}
結果は以下のようになった。
9007199254740981 / 3 = 3002399751580327
9007199254740982 / 3 = 3002399751580327.5
9007199254740983 / 3 = 3002399751580327.5
9007199254740984 / 3 = 3002399751580328
9007199254740985 / 3 = 3002399751580328.5
9007199254740986 / 3 = 3002399751580328.5
9007199254740987 / 3 = 3002399751580329
9007199254740988 / 3 = 3002399751580329.5
9007199254740989 / 3 = 3002399751580329.5
9007199254740990 / 3 = 3002399751580330
9007199254740991 / 3 = 3002399751580330.5
3 で割り切れる値を割った場合は整数に、3 で割り切れない値を割った場合は小数部分が .5
の値になっている。
これは、3 で割った場合の小数部分は .0
か .33333…
か .66666…
のいずれかであり、.33333…
も .66666…
も一番近い表現できる値に丸めると .5
になるためであると考えられる。
Number.MAX_SAFE_INTEGER
の値 9007199254740991
を2進数で表すと
11111111111111111111111111111111111111111111111111111
となり、これは53ビットの数である。
この「53ビット」というのは、浮動小数点数の仮数部の52ビットに、データから省略される最上位の「1」のビットを合わせた数に対応している。
Number.MAX_SAFE_INTEGER
を3で割った商 3002399751580330
を2進数で表すと
1010101010101010101010101010101010101010101010101010
となり、これは52ビットの数である。
1ビット減っているので、この1ビットを小数点以下の部分を表すのに使うことができ、小数部分が .0
の数と .5
の数を表現できる。
さて、ここで ECMAScript における /
演算子の動作の定義を確認しよう。
13.7 Multiplicative Operators より、/
演算子は EvaluateStringOrNumericBinaryExpression
を用いて処理されることがわかる。
EvaluateStringOrNumericBinaryExpression
は、ApplyStringOrNumericBinaryOperator
を用いて処理されることがわかる。
ApplyStringOrNumericBinaryOperator
において、Number
同士の除算は Number::divide
を用いて処理されることがわかる。
Number::divide
において、±0・±∞・NaN 以外の除算 x / y
は
Return 𝔽(ℝ(x) / ℝ(y)).
と定義されている。
ℝ(x) は「x が表す (数学的な) 実数」である。
𝔽(x) は「x に一番近い浮動小数点数」(IEEE 754-2019 roundTiesToEven) である。
よって、除算の結果は一番近い表現できる値に丸められることがわかる。
よって、3 で割ったとき、割り切れる場合は小数部分が .0
に、割り切れない場合は小数部分が .0
以外になり、適切な方法で丸めれば正確な整数の結果が得られると考えられる。
4以上で割る場合
割る数をさらに大きくしていくと、商はさらに小さくなっていき、小数点以下の部分を表現するのに用いることが出来るビットが増えていく。
そのため、商をより精度良く表現でき、適切な方法で丸めれば正確な整数の結果が得られると考えられる。
丸める適切な方法
JavaScript で大きい整数の除算結果を整数で求めたいとき、
~~(a / b)
のようにしてはいけない。
なぜなら、~
演算子は入力を32ビット符号付き整数に変換して処理し、大きい整数は切り詰められてしまうからである。
Math.floor(a / b)
のように Math.floor
を用いることで、小数点以下を切り捨てた整数の商を求めることができる。
ただし、商が負の場合は、これを用いると結果の絶対値が丸める前より大きくなることがある。(-2.5
→ -3
など)
負の数を扱う可能性がある場合は、行いたい計算の内容に応じて適切な方法を選択するべきである。
結論
JavaScript の Number.MIN_SAFE_INTEGER
以上 Number.MAX_SAFE_INTEGER
の整数について、
- 加算・減算・乗算は、計算結果もこの範囲に収まるならば、正確に行えそうである
- 乗算は、
Math.floor
などの適切な丸めを行うことで、正確に行えそうである
ことを確かめることができた。