JavaScriptの数値表現とIEEE754が生む落とし穴
0.1+0.2が0.3にならない理由が原理から腑に落ちる。全数値が倍精度であるNumberの帰結、安全整数の限界、ビット演算の32bit変換、BigIntが必要な場面まで一気に整理します。
- 1.JavaScriptのNumberは唯一の数値型で、すべてIEEE754倍精度(64bit)。整数も小数も同じ箱に入るため、0.1のような10進小数は2進で正確に表せず誤差が出る。
- 2.整数として安全なのは仮数52bitに収まる範囲だけで、上限はNumber.MAX_SAFE_INTEGERの2^53-1。これを超えると整数の一意性が崩れ、隣り合う整数が同じ値になる。
- 3.ビット演算はオペランドを符号付き32bit整数へ変換してから計算する。53bit超の整数や正確な大整数演算が必要ならBigInt(任意精度整数)を使う。
なぜ「数値の表現」を知る必要があるのか
0.1 + 0.2 が 0.3 にならない、巨大なIDが勝手に書き換わる、ビット演算の結果が想定とずれる——これらは個別のバグではなく、JavaScriptの数値型が一種類しかないという設計から導かれる必然の帰結です。原理を押さえれば、どの計算が危険でどこにガードを入れるべきかを推測でなく仕組みから判断できます。前提として基礎は JavaScript を押さえておくと、ここでの内部表現の話がつながります。
Number はすべて IEEE754 倍精度
ECMAScript仕様では、Number は唯一の数値型(後述の BigInt は別型)であり、その実体は IEEE754の倍精度浮動小数点(binary64, 64bit) です。整数の 42 も小数の 3.14 も、内部では同じ64bitのフォーマットで表現されます。64bitの内訳は次の通りです。
| 部分 | ビット幅 | 役割 |
|---|---|---|
| 符号 (sign) | 1bit | 正負を表す |
| 指数 (exponent) | 11bit | 2の何乗かを表す(バイアス1023付き) |
| 仮数 (mantissa/fraction) | 52bit | 有効数字。先頭の1は省略され実質53bit精度 |
値はおおむね 符号 × 1.仮数(2進) × 2^指数 という形で表されます。仮数が52bit+省略された先頭1bitで合計53bitの有効精度を持つ、という点がこの記事のすべての落とし穴の根っこです。
0.1 + 0.2 問題:2進で割り切れない小数
0.1 を2進数で表すと、0.0001100110011… と無限循環します。10進で 1/3 = 0.333… が割り切れないのと同じことが、2進では 1/10 で起きます。binary64は仮数53bitで打ち切るため、0.1 も 0.2 も最も近い表現可能な値に丸めた近似値として格納されます。その近似どうしを足すと、丸め誤差が積み上がって 0.3 のちょうどの近似とはわずかに食い違います。
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // false
(0.1).toPrecision(20);// "0.10000000000000000555"
これはJavaScript固有のバグではなく、binary64を使う言語(C、Java、Pythonのfloatなど)すべてに共通する挙動です。実務上の対処は次の通りです。
- 比較は誤差を許容する:
Math.abs(a - b) < Number.EPSILONのように許容誤差(イプシロン)で判定する。 - 金額は最小単位の整数で扱う:円なら円、ドルならセント単位の整数で計算し、表示時だけ小数へ戻す。
- 正確な10進演算が要るなら専用ライブラリ:decimal系のライブラリで10進固定小数点として扱う。
誤差は乗算・除算でも蓄積します。とくに大きな値と極小の値を足すと、小さい側が仮数の精度の外へ追い出されて消える(吸収誤差)。1e16 + 1 === 1e16 が true になるのはこのためで、53bit精度を超えた瞬間に「+1」が表現できなくなります。
Number.MAX_SAFE_INTEGER:整数が信用できる上限
浮動小数点は小数だけでなく大きな整数でも精度を失います。仮数の有効精度は53bitなので、絶対値が 2^53 未満の整数は1つずつ正確に表せますが、それを超えると表現できる整数が飛び飛びになります。この境界が Number.MAX_SAFE_INTEGER、すなわち 2^53 - 1 = 9007199254740991 です。
Number.MAX_SAFE_INTEGER; // 9007199254740991 (2^53 - 1)
Number.MAX_SAFE_INTEGER + 1; // 9007199254740992 (まだ正確)
Number.MAX_SAFE_INTEGER + 2; // 9007199254740992 (本来は ...993 のはず!)
9007199254740993 === 9007199254740992; // true
2^53 を超えると、隣り合う整数が同じ64bit値に丸められ、異なる整数が等しく見える事故が起きます。Number.isSafeInteger(n) で安全範囲内かを判定でき、範囲外なら +1 や === の結果はもはや信用できません。
Number.MAX_VALUE(約1.8e308)は「表現できる最大の有限値」で、Number.MAX_SAFE_INTEGER(約9e15)は「整数を1刻みで正確に保てる上限」です。前者ははるかに大きいですが、その領域では整数は飛び飛びにしか表せません。資格試験などでは安全整数の上限は2^53-1を問われやすい点に注意してください。
実務で特に危ないのが、外部システムの64bit整数ID(DBのBIGINT主キー、Twitter/X系のSnowflake ID、一部の金融取引IDなど)です。これらは53bitを超え得るため、JSONを JSON.parse した時点で Number へ落ちると静かに値が壊れます。対策は文字列のまま受け渡すか、後述の BigInt で扱うことです。
ビット演算の32bit変換:64bitがいったん畳まれる
& | ^ ~ << >>(符号なし右シフト >>> を含む)のビット演算は、Number が64bit浮動小数点であるにもかかわらず、内部仕様でオペランドを符号付き32bit整数へ変換してから計算します(>>> のみ符号なし32bitとして結果を返す)。つまり53bitまで正確に持てる整数でも、ビット演算を通した瞬間に下位32bitだけが使われ、上位は捨てられます。
(2 ** 31) | 0; // -2147483648 (32bit符号付きで負に折り返す)
(2 ** 32) | 0; // 0 (下位32bitがすべて0)
(2 ** 33 + 5) | 0; // 5 (上位ビットは消える)
-1 >>> 0; // 4294967295 (符号なし32bitとして解釈)
x | 0 や ~~x が「小数を整数に切り捨てるイディオム」として使われるのはこの変換のためですが、値が 2^31 以上だと符号付き32bitの範囲を超えて意図しない負数になります。32bitを超える整数のビット操作が必要なら BigInt を使い、Math.trunc で済む切り捨てに無理にビット演算を流用しないのが安全です。
BigInt:53bitの壁を越える任意精度整数
ここまでの限界——53bit超の整数が壊れる、ビット演算が32bitに畳まれる——を根本から解くために、ES2020で BigInt が導入されました。BigInt は任意精度の整数で、メモリが許す限り桁数の上限がありません。リテラルは末尾に n を付け、BigInt() でも生成します。
9007199254740993n + 1n; // 9007199254740994n (正確!)
2n ** 64n; // 18446744073709551616n
(1n << 40n) | 0xffn; // 32bitに畳まれず正確にビット演算できる
typeof 10n; // "bigint" (Number とは別の型)
| 観点 | Number (binary64) | BigInt |
|---|---|---|
| 表現 | 倍精度浮動小数点 | 任意精度の整数のみ |
| 整数の正確さ | 2^53-1 まで | 桁数に上限なし |
| 小数 | 扱える(誤差あり) | 扱えない(整数専用) |
| ビット演算 | 32bitへ変換 | 幅の制限なし |
| 性能 | ハードウェアで高速 | ソフト実装でやや遅い |
注意点として、Number と BigInt は暗黙には混ぜられません。1n + 1 は TypeError になり、どちらかへ明示変換が必要です。また BigInt は整数専用なので小数は表せず、5n / 2n は 2n(切り捨て)になります。したがって万能の置き換えではなく、正確さが必須の大整数(ID・暗号・厳密な会計計算など)に限定して使い、小数を含む計算や性能重視のループでは引き続き Number を選ぶのが定石です。Number の最適化挙動(小整数Smiの扱いなど)は JavaScriptエンジンの内部(JITとインラインキャッシュ) も合わせて見ると理解が深まります。
まとめ
JavaScriptの Number は唯一の数値型でIEEE754倍精度64bit、有効精度は53bit——この一点から落とし穴がすべて派生します。0.1 のような10進小数は2進で割り切れず丸め誤差を生み、0.1 + 0.2 !== 0.3 となる。整数を1刻みで信用できるのは Number.MAX_SAFE_INTEGER(2^53-1)まで、これを超えると整数が飛び飛びになり64bit IDが壊れる。ビット演算はオペランドを32bit整数へ畳むため上位ビットが失われる。これらの限界を超える正確な整数演算には任意精度の BigInt を使う。「全数値が浮動小数点」という前提を起点に各挙動を導けるようになれば、数値バグは推測でなく仕組みから防げます。より低レイヤの数値表現は WebAssemblyの実行モデル でも触れています。
Web/フロントエンド Article
JavaScriptの数値表現とIEEE754が生む落とし穴を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JavaScript
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
整数として安全なのは仮数52bitに収まる範囲だけで、上限はNumber.MAX_SAFE_INTEGERの2^53-1。これを超えると整数の一意性が崩れ、隣り合う整数が同じ値になる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「JavaScript / IEEE754」に近いか確認する。
- 強みである「JavaScriptのNumberは唯一の数値型で、すべてIEEE754倍精度(64bit)。整数も小数も同じ箱に入るため、0.1のような10進小数は2進で正確に表せず誤差が出る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。