整数オーバーフローと BigInt:言語別の安全な上限
INT_MAX + 1 を C・Java・C#・Swift・Dart・PHP・Python で実行して比較。JSON.parse で 19 桁の ID が変わる理由と文字列・BigInt での対策、SQLite の上限まで確認します。
シリーズ:数値・お金の計算講座 2/6(初級)。 金額やカウンター、ID を整数で持っていて、「整数は float と違って常に正確」と思っている新人エンジニア向けです。読み終えると、各言語の整数の大きさ、上限を超えたときに何が起きるか、19 桁の ID を JSON と JavaScript で桁を失わずに扱う方法が分かります。
第 1 回では小数が近似値であることを見ました。整数は正確ですが、それは範囲の内側だけの話で、端で何が起きるかは言語ごとにまったく違います。C は未定義動作、Java は黙って一周、Swift はプログラムを止め、PHP は float に化け、JavaScript は静かに丸めます。受発注システム、決済 API、何年も動き続けるカウンター、DB の ID を JSON で受け取るフロントエンドを作るなら、どれかには必ず出会います。円は小数を持たないので金額を整数で持つのは日本ではごく普通ですが、32 ビットの int に入るのは約 21 億 4748 万までです。以下はすべて Apple Silicon Mac(M1 Pro)、macOS 26 で、Apple clang 21、OpenJDK 26、.NET 10(SDK 10.0.403)、Swift 6.4.2、Dart 3.11.5、PHP 8.5.6、Python 3.9.6 と 3.12.14、JavaScriptCore の jsc、sqlite 3.51.0 を使って実行しました。パスは ~/demo/ に置き換えています。
5 分で最初のオーバーフロー
単価 2,000,000 円の商品を 1,500 個、合計 30 億円です。first_ov.c として保存します。
#include <stdio.h>
int main(void) {
int price = 2000000;
int qty = 1500;
int total = price * qty;
printf("total = %d\n", total);
long long ok = (long long)price * qty;
printf("ok = %lld\n", ok);
return 0;
}
$ clang -std=c17 -Wall -Wextra -o first_ov first_ov.c && ./first_ov
total = -1294967296
ok = 3000000000
コンパイル時の警告もクラッシュもなく、合計がマイナスになるだけです。この Mac の C の int は 32 ビットで、-2,147,483,648 から 2,147,483,647(約 21 億 4748 万)までしか入りません。30 億は入らず、残ったビットを読むと -1,294,967,296 になります。計算結果が型の範囲を超えることを整数オーバーフローと呼びます。
対策は、掛け算を広い型で行うことです。(long long)price * qty は掛ける前に片方を 64 ビットにします。long long total = price * qty; では手遅れで、掛け算は 32 ビットで終わっています。
-fsanitize=undefined(UBSan、未定義動作サニタイザ)を付けると、clang が該当行を教えてくれます。
$ clang -std=c17 -Wall -Wextra -fsanitize=undefined -o first_ov_ub first_ov.c && ./first_ov_ub
first_ov.c:5:23: runtime error: signed integer overflow: 2000000 * 1500 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior first_ov.c:5:23
total = -1294967296
ok = 3000000000
整数の大きさ:言語別一覧
| 言語 | 既定の整数 | 範囲 | もっと大きいもの |
|---|---|---|---|
| C(clang、macOS arm64) | int 32 ビット |
±約 21 億 | long / long long 64 ビット(ここでは sizeof が 8 と 8) |
| Java | int 32 ビット |
±約 21 億 | long 64 ビット、BigInteger |
| C# | int 32 ビット |
±約 21 億 | long 64 ビット、decimal、BigInteger |
| Swift | Int(64 ビット環境で 64 ビット) |
±約 922 京 | Int64、標準の多倍長整数はなし |
| Dart(ネイティブ) | int 64 ビット |
±約 922 京 | BigInt |
| Dart(JavaScript に変換) | int は JS の数値 |
2^53 まで正確 | BigInt |
| PHP | int 64 ビット(PHP_INT_SIZE 8) |
±約 922 京 | bcmath、gmp |
| Python | int |
無制限 | 不要 |
| JavaScript | Number(double) |
2^53 - 1 まで正確 | BigInt |
| SQLite | INTEGER 最大 8 バイト |
±約 922 京 | TEXT で保存 |
覚えておく数は 2 つです。2,147,483,647(2^31 - 1、21 億 4748 万 3647、32 ビットの上限)と 9,007,199,254,740,991(2^53 - 1、約 9007 兆、double が隙間なく持てる最大の整数)。64 ビットの上限は 9,223,372,036,854,775,807(約 922 京)です。
上限を超えたとき:7 言語の比較
どの言語でも MAX + 1 と 2,000,000 × 1,500 を計算しました。出力は実行結果のままです。
C:符号付き整数のオーバーフローは未定義動作(UB)です。規格は何が起きるかを決めておらず、最適化器は「起きない」と仮定してよいことになっています。この Mac では INT_MAX + 1 が -2147483648 でしたが、たまたまで規則ではありません(-O2 で結果が変わるループは C の未定義動作と最適化 を参照)。符号なしのオーバーフローは定義されていて、UINT_MAX + 1 は 0 でした。UB なしで確認するには clang/gcc の組み込み関数 __builtin_add_overflow(a, 1, &r) を使い、INT_MAX で真を返しました。-fno-sanitize-recover=all を付けると UBSan は最初の報告でプログラムを止め、終了ステータスは 134 でした。
Java:常に黙って一周します。
a + 1 = -2147483648
price * qty = -1294967296
long wrong = -1294967296 // long wrong = price * qty;
long right = 3000000000 // long right = (long) price * qty;
Math.abs(Integer.MIN_VALUE) = -2147483648
java.lang.ArithmeticException: integer overflow // Math.addExact(a, 1)
java.lang.ArithmeticException: integer overflow // Math.multiplyExact(price, qty)
最小の int の Math.abs はマイナスのままです。+2,147,483,648 は int に存在しないからです。*Exact 系メソッド(Java 8 以降)が安全版です。
C#:既定では一周し、checked でオーバーフローを例外にできます。定数はコンパイル時にチェックされます。
price * qty = -1294967296
OverflowException: Arithmetic operation resulted in an overflow. // checked(price * qty)
Program.cs(1,9): error CS0220: The operation overflows at compile time in checked mode
最後の行は int x = int.MaxValue + 1; のコンパイルエラーです。SDK のメッセージは OS の言語設定で翻訳されることがあるので、検索するときはコード CS0220 で探すのが確実です。英語で出したいときは DOTNET_CLI_UI_LANGUAGE=en dotnet build とします(上の行はそうして取りました)。.csproj に <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> を書くとプロジェクト全体がチェック付きになります。dotnet new console で作ったばかりのプロジェクトには入っていません。
Swift:オーバーフローするとトラップ、つまり意図的にプログラムを止めます。定数ならコンパイルすら通りません。
ovc.swift:1:26: error: arithmetic operation '2147483647 + 1' (on type 'Int32') results in an overflow
実行時に決まる値では、about to multiply を表示したあと終了ステータス 133(128 + シグナル 5、SIGTRAP)で終わりました。-Onone でも -O でも同じです。一周させたいときは明示します。a &+ 1 は -2147483648、a.addingReportingOverflow(1) は (-2147483648, true) でした。
Dart:どこで動かすかで変わります。Dart VM では int は 64 ビットで一周し、9223372036854775807 + 1 は -9223372036854775808 でした。dart compile js で JavaScript に変換すると int は JS の数値になるので、表せないリテラルはコンパイラが拒否します。
Error: The integer literal 9007199254740993 can't be represented exactly in JavaScript.
int.parse で読んだ値の場合、変換後のコードを(ブラウザではなく jsc で)実行すると、64 ビットの最大値が 9223372036854776000、2^53 + 1 が 9007199254740992 になり、identical(1, 1.0) は true になりました。同じソースで数字が変わります。Flutter Web は別にテストしてください。
PHP:あふれた int は float になります。
var_dump(PHP_INT_MAX + 1); float(9.223372036854776E+18)
var_dump(PHP_INT_MAX + 1 == PHP_INT_MAX); bool(true)
(int)(PHP_INT_MAX + 1) -> Warning: The float 9.223372036854776E+18 is not representable as an int, cast occurred
int(-9223372036854775808)
intdiv(PHP_INT_MIN, -1) -> Fatal error: Uncaught ArithmeticError: Division of PHP_INT_MIN by -1 is not an integer
エラーにはなりませんが、float の精度は 53 ビットなので PHP_INT_MAX + 1 == PHP_INT_MAX が真になります。int に戻すキャストは 8.5.6 で警告を出しました。古い版で出るかは試していません。bcadd("9223372036854775807", "1") と gmp_add はどちらも正確に 9223372036854775808 を表示しました。
Python:整数はあふれません。(2**63 - 1) + 1 は 9223372036854775808、2**64 * 2**64 は 39 桁すべてを表示しました。限界は固定長の世界に渡すときに出ます。struct.pack('<i', 2**31) は struct.error: 'i' format requires -2147483648 <= number <= 2147483647、(2**64).to_bytes(8, 'little') は OverflowError: int too big to convert でした。
JavaScript:MAX_SAFE_INTEGER と桁が変わる ID
JavaScript の Number は double(第 1 回)です。53 ビットの仮数で 2^53 までの整数はすべて正確に持てますが、その先は double の間隔が 2、4、8… と広がります。
$ jsc safe.js
9007199254740991 true // Number.MAX_SAFE_INTEGER、=== 2**53 - 1
9007199254740992 9007199254740992 true // 2**53 と 2**53 + 1 が等しい
true false // Number.isSafeInteger(2**53 - 1), (2**53)
9007199254740992 9007199254740992 9007199254740994 9007199254740996 9007199254740996 // 2**53 + 0..4
これがいちばん効くのは ID です。64 ビットの自動採番キーや、分散システムの Snowflake 方式の ID は 2^53 を超えます。サーバーは正しい JSON を返しているのに、ブラウザの JSON.parse が黙って値を変えます。
const body = '{"id": 1234567890123456789, "order_id": 9007199254740993, "sid": "1234567890123456789"}';
const o = JSON.parse(body);
print(o.id, o.order_id, o.sid);
print(JSON.stringify(o));
1234567890123456800 9007199254740992 1234567890123456789
{"id":1234567890123456800,"order_id":9007199254740992,"sid":"1234567890123456789"}
ID は 11、注文 ID は 1 ずれ、エラーは何も出ません。クライアントがその ID を送り返せば、別の行を更新するか、何も更新しません。文字列の sid は無事でした。Python の json.loads と PHP 8.5 の json_decode は整数が大きいので 9007199254740993 を正しく読みます。PHP は 64 ビットを超える 12345678901234567890 を float(1.2345678901234567E+19) にし、JSON_BIGINT_AS_STRING を渡すと string(20) "12345678901234567890" になりました。サーバー側の似た罠は PHP のプリペアドステートメントで JSON の数値型が変わる話 にあります。
対策(優先順):
- ID は文字列で送る。 API は
"id": "1234567890123456789"を返します。ID はラベルで、足し算はしません。どのクライアントでも動きます。 - 計算には
BigInt。BigInt("1234567890123456789") + 1nは1234567890123456790でした。BigInt には独自の規則があり、jscで確認しました。
1n + 1 TypeError: Invalid mix of BigInt and other type in addition.
JSON.stringify({x:10n}) TypeError: JSON.stringify cannot serialize BigInt.
Math.sqrt(16n) TypeError: Conversion from 'BigInt' to 'number' is not allowed.
- パース中に元の文字列を読む。 新しいエンジンでは
JSON.parseの reviver に各数値の元テキストが渡され、JSON.rawJSONで BigInt を数値のまま書き戻せます。この Mac のjscではどちらも動きました。
const r = JSON.parse(body, (k, v, ctx) => k === 'id' ? BigInt(ctx.source) : v);
print(String(r.id), typeof r.id); // 1234567890123456789 bigint
print(JSON.stringify({ id: JSON.rawJSON(r.id.toString()) })); // {"id":1234567890123456789}
この 2 つは言語に最近加わった機能です。対応するすべてのブラウザと Node.js の版にあるか確認してから使ってください(実行したのは jsc だけです)。1 の方法にはこの注意は要りません。
もう一つの静かな 32 ビットの壁:ビット演算は 32 ビット整数に変換します。(2147483647 + 1) | 0 は -2147483648、Math.imul(2000000, 1500) は -1294967296 でした。Number と BigInt の詳細は JavaScript のパフォーマンスと数値 で扱っています。
データベース:SQLite の INTEGER の上限
SQLite の INTEGER は符号付き 64 ビットです。上限では 3 通りの挙動をします。
sqlite> SELECT 9223372036854775807 + 1, typeof(9223372036854775807 + 1);
9.22337203685478e+18|real
sqlite> SELECT sum(x) FROM (SELECT 9223372036854775807 AS x UNION ALL SELECT 1);
Runtime error near line 4: integer overflow
sqlite> SELECT total(x) FROM (SELECT 9223372036854775807 AS x UNION ALL SELECT 1);
9.22337203685478e+18
+ は黙って float に切り替わり、sum() はエラー、total() は常に float を返します。INTEGER 列に 12345678901234567890 を入れると real で保存され、引用符付きの同じ数字も、列が整数アフィニティなので real になりました。64 ビットを超える値は TEXT 列に入れます。
主キーが上限に達したとき:
sqlite> CREATE TABLE t(id INTEGER PRIMARY KEY, n INTEGER);
sqlite> INSERT INTO t(id, n) VALUES (9223372036854775807, 1);
sqlite> INSERT INTO t(n) VALUES (2);
sqlite> SELECT * FROM t;
712334064962770860|2
9223372036854775807|1
最大の rowid が使われていると、SQLite は空いている値をランダムに選びます。再利用も逆戻りもしないことを約束する AUTOINCREMENT 付きでは、同じ挿入が Error: stepping, database or disk is full (13) で失敗しました。数えて 922 京に届くことはありませんが、誰かが巨大な ID を手で入れれば届きます。SQL の基本は SELECT・WHERE・ORDER BY を参照してください。
よくあるミスと型の選び方
| ミス | 実際に起きること | 直し方 |
|---|---|---|
int a, b で long long t = a * b;(Java の long t = a * b も) |
先に 32 ビットで掛け算してあふれる:-1294967296 |
片方を広げる:(long long)a * b |
ミリ秒を int で数える |
2^31 ミリ秒は約 24.8 日 | 64 ビットにする |
非負にするつもりの Math.abs(x) |
Math.abs(Integer.MIN_VALUE) は -2147483648 |
最小値を確認するか long に |
| JS クライアントに数値の ID を JSON で送る | 1234567890123456789 が 1234567890123456800 で届く |
ID は文字列で送る |
BigInt と Number を混ぜる |
TypeError: Invalid mix of BigInt and other type |
明示的に変換し、値ごとに型を一つに |
| 2^63 を超えても PHP の int 演算を信じる | 黙って float になり、== PHP_INT_MAX が真 |
bcmath か gmp |
| 一つの Dart コードを VM と Web で共有 | Web で 9223372036854775807 が 9223372036854776000 に |
2^53 未満に収めるか BigInt |
型の選び方:
| 保存するもの | 使う型 | 避ける型 |
|---|---|---|
| 個数、数量、約 9×10^15 未満の最小通貨単位(円・セント) | 64 ビット整数(long、Int、PHP/Dart VM の int、INTEGER) |
合計が伸びるなら 32 ビット int |
| DB や他サービスの ID | JSON と JS では文字列、サーバーでは 64 ビット整数 | JS の Number |
| 2^63 を超えて計算が必要な整数 | BigInteger、Python の int、BigInt、gmp |
float |
| 黙ってあふれてはいけない値 | Java の *Exact、C# の checked、Swift の通常の演算子、__builtin_*_overflow |
C/Java/C# の素の + |
お金(銭・セント、decimal 型)は第 4 回で扱います。型の基本は Java の型 と PHP の型 にあります。
計測:JSON.parse を無事に通る ID の割合と BigInt のコスト
JSON.parse を無傷で通る ID はどれくらいか。 5 つの範囲それぞれで Python(random.seed(7))で乱数整数を 100,000 個作り、JSON の数値と文字列の両方で書き出して jsc で読み、数値を文字列と BigInt で比べました。
| 範囲 | 無事だった数 | 最大の誤差 |
|---|---|---|
| 2^53 - 1 以下 | 100,000(100%) | 0 |
| 2^53〜2^54 | 49,994(49.99%) | 1 |
| 2^54〜2^56 | 16,718(16.72%) | 4 |
| 2^59〜2^61(Snowflake 程度) | 526(0.53%) | 128 |
| 10^18〜2^63(19 桁) | 189(0.19%) | 512 |
2^53 を超えると、無事なのは間隔のちょうど倍数になっている ID だけです。19 桁の ID は 99.8% が最大 512 ずれて出てきます。
BigInt のコストは? jsc で 0〜9,999,999 を合計、Apple Silicon Mac(M1 Pro)、7 回の中央値で、Number 3.2 ms、BigInt 301.3 ms、結果はどちらも 49999995000000 でした。JIT が Number のループを強く最適化するので、約 94 倍という差は上限の目安で、定数ではありません。Python の任意長 int で 1,000,000 個を足し込んだ時間(7 回の中央値):
| 初期値 | Python 3.9.6 | Python 3.12.14 |
|---|---|---|
| 0 | 27.7 ms | 26.6 ms |
| 2**64 | 28.1 ms | 26.9 ms |
| 2**256 | 30.6 ms | 29.0 ms |
| 2**4096 | 141.8 ms | 138.0 ms |
大きな整数はただではありませんが、数百ビットまでなら Python はほとんど差を見せません。BigInt は必要な所だけで使えば十分です。
正直な限界とバージョンの注記
- C の整数の大きさは macOS arm64 の clang のものです。64 ビット Windows では
longが 32 ビットです(ここでは実行していません)。 - C の符号付きオーバーフローの結果は、このコンパイラが
-O0で出したものです。規格は何も保証しません。 - Swift のトラップは、取得した出力ではメッセージがなく終了ステータス 133 だけでした。Xcode のデバッガではもっと表示されます(ここでは実行していません)。
JSON.parseの元テキスト取得とJSON.rawJSONは、この Mac のjscでだけ確認しました。Python の 4,300 桁制限は、3.12.14 ではValueError: Exceeds the limit (4300 digits) for integer string conversionになり、3.9.6 では 5,001 桁を表示しました。制限が入ったのは 3.9.14 からです。- Dart から変換した JavaScript は
jscで実行し、ブラウザでは試していません。Node.js も実行していません。 - 時間は 1 台での計測です。比率を見てください。
練習問題
1. Java で 3000000000 を表示するのはどれですか。
int price = 2_000_000, qty = 1500;
long a = price * qty;
long b = (long) price * qty;
long c = (long) (price * qty);
2. API が {"id": 9007199254740993} を返し、Web クライアントはその ID を送り返してレコードを削除します。jsc では ID が何になり、API はどう変えるべきですか。
3. Python で x = 2**31 は問題ありません。同じ値がエラーになる場所を一つ挙げ、エラーメッセージを書いてください。
解答
1. b だけです。実行結果は a b c の順に -1294967296 3000000000 -1294967296 でした。a と c は先に 32 ビットで掛け算しており、c のキャストはオーバーフローの後です。
2. JSON.parse の結果は 1 小さい 9007199254740992 で、クライアントは ...993 ではなく ...992 を削除しようとします。ID を "id": "9007199254740993" と文字列で返し、クライアントでも文字列のまま扱います。
3. struct.pack('<i', 2**31) が struct.error: 'i' format requires -2147483648 <= number <= 2147483647 を出しました。バイナリ形式のファイル、C のライブラリ、32 ビットの DB 列に書くときにも同じことが起きます。
次回
第 3 回は丸めです。Python の round(2.5) がなぜ 2 なのか、toFixed が実際に何をするのか、消費税を明細ごとに丸めるか請求書全体で丸めるかで結果がどう変わるかを見ます。
よくある質問
JSON.parse で ID の下の桁が変わってしまうのはなぜ?
JavaScript の数値は double で、すべての整数を正確に持てるのは 2^53 - 1(Number.MAX_SAFE_INTEGER、9007199254740991)までです。それを超えると最も近い double が選ばれます。jsc では 1234567890123456789 が 1234567890123456800 に、9007199254740993 が 9007199254740992 になりました。大きな ID は JSON の文字列で送り、計算が必要なときだけ BigInt に変換します。
Java や C# で int があふれるとどうなる?
既定ではどちらも黙って一周します。Integer.MAX_VALUE + 1 は -2147483648、2,000,000 × 1,500 は -1294967296 になりました。Java の Math.addExact や Math.multiplyExact は ArithmeticException: integer overflow を投げ、C# は checked(...) の中や CheckForOverflowUnderflow を有効にしたときに OverflowException を投げます。
Python の整数はオーバーフローしますか?
しません。必要に応じて桁が伸び、(2**63 - 1) + 1 は 9223372036854775808 と表示されました。限界は外の世界との境目に出ます。struct.pack('<i', 2**31) は struct.error になり、2022 年のセキュリティリリース(3.11、3.10.7、3.9.14 以降)からは 4,300 桁を超える int の文字列変換が既定で ValueError になります。