Desbordamiento de enteros, BigInt y límites seguros
INT_MAX + 1 en C, Java, C#, Swift, Dart, PHP y Python; por qué JSON.parse cambia IDs de 19 dígitos, el arreglo con strings o BigInt y los límites de SQLite.
Serie: curso de números y dinero 2/6 (principiante). Para quien guarda precios, contadores o IDs en enteros y supone que los enteros, a diferencia de los floats, siempre son exactos. Al terminar vas a saber qué tan grandes son los enteros de cada lenguaje, qué pasa cuando un cálculo se pasa del límite y cómo mover IDs de 19 dígitos por JSON y JavaScript sin perder dígitos.
La lección 1 mostró que los floats son aproximados. Los enteros son exactos, pero solo dentro de su rango, y en el borde cada lenguaje hace algo distinto: C tiene comportamiento indefinido, Java da la vuelta en silencio, Swift detiene el programa, PHP convierte el número en float y JavaScript lo redondea sin avisar. Las monedas sin centavos de uso práctico, como el peso chileno o el colombiano, producen montos grandes muy rápido: una factura de 2,000,000 de pesos por 1,500 unidades ya son 3,000 millones, y eso no cabe en un int de 32 bits. Si haces un sistema de pedidos, una API de pagos, un contador que corre por años o un front end que recibe IDs de la base de datos en JSON, te vas a topar con alguno de estos casos. Todo lo de abajo se ejecutó en una Mac con Apple Silicon (M1 Pro), macOS 26, con 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 y 3.12.14, JavaScriptCore jsc y sqlite 3.51.0. Las rutas aparecen acortadas como ~/demo/.
Tu primer desbordamiento en 5 minutos
Un precio de 2,000,000 (pesos, centavos, da igual) por una cantidad de 1,500 son 3,000,000,000. Guarda esto como 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
Ni advertencia al compilar ni caída: solo un total negativo. Un int de C en esta Mac tiene 32 bits, así que va de -2,147,483,648 a 2,147,483,647. Tres mil millones no caben; los bits que quedan se leen como -1,294,967,296. Esto se llama desbordamiento de enteros (integer overflow): el resultado de un cálculo queda fuera del rango de su tipo.
El arreglo es multiplicar en un tipo más ancho. (long long)price * qty convierte un operando a 64 bits antes de multiplicar. Escribir long long total = price * qty; es tarde: la multiplicación ya ocurrió en 32 bits.
Agrega -fsanitize=undefined (UBSan, el detector de comportamiento indefinido) y clang te dice la línea exacta:
$ 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
¿Qué tan grande es un entero? Tamaños por lenguaje
| Lenguaje | Entero por defecto | Rango | Opción más grande |
|---|---|---|---|
| C (clang, macOS arm64) | int de 32 bits |
±2.1×10^9 | long / long long de 64 bits (sizeof 8 y 8 aquí) |
| Java | int de 32 bits |
±2.1×10^9 | long de 64 bits, BigInteger |
| C# | int de 32 bits |
±2.1×10^9 | long de 64 bits, decimal, BigInteger |
| Swift | Int, de 64 bits en plataformas de 64 bits |
±9.2×10^18 | Int64, sin entero grande integrado |
| Dart (nativo) | int de 64 bits |
±9.2×10^18 | BigInt |
| Dart (compilado a JavaScript) | int es un número de JS |
exacto hasta 2^53 | BigInt |
| PHP | int de 64 bits (PHP_INT_SIZE 8) |
±9.2×10^18 | bcmath, gmp |
| Python | int |
ilimitado | no hace falta |
| JavaScript | Number (un double) |
exacto hasta 2^53 - 1 | BigInt |
| SQLite | INTEGER de hasta 8 bytes |
±9.2×10^18 | guardar como TEXT |
Vale la pena memorizar dos números: 2,147,483,647 (2^31 - 1, el límite de 32 bits) y 9,007,199,254,740,991 (2^53 - 1, el entero más grande que un double guarda sin huecos). El límite de 64 bits es 9,223,372,036,854,775,807.
Qué pasa en el borde: siete lenguajes comparados
Calculamos MAX + 1 y 2,000,000 × 1,500 en todos. Las salidas están copiadas de las ejecuciones.
C: el desbordamiento con signo es comportamiento indefinido (UB): el estándar no dice qué pasa, y el optimizador puede asumir que nunca ocurre. En esta Mac INT_MAX + 1 imprimió -2147483648, pero es suerte, no una regla (mira C undefined behaviour and optimization (en inglés) para un bucle que cambia con -O2). El desbordamiento sin signo sí está definido: UINT_MAX + 1 imprimió 0. Para comprobar sin UB, usa __builtin_add_overflow(a, 1, &r) (una función integrada de clang/gcc), que devolvió verdadero para INT_MAX. Con -fno-sanitize-recover=all, UBSan detiene el programa en el primer reporte; el estado de salida fue 134.
Java: da la vuelta, siempre, en silencio.
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)
El Math.abs del int más pequeño sigue siendo negativo, porque +2,147,483,648 no existe en un int. Los métodos *Exact (Java 8 en adelante) son las versiones seguras.
C#: da la vuelta por defecto; checked convierte el desbordamiento en excepción, y las constantes se revisan al compilar.
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
La última línea salió de int x = int.MaxValue + 1;, que no compila. Los mensajes del SDK pueden salir traducidos según el idioma del sistema; busca por el código CS0220, o fuerza inglés con DOTNET_CLI_UI_LANGUAGE=en dotnet build (así obtuvimos esa línea). Un interruptor para todo el proyecto, <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> en el .csproj, hace que toda la aritmética se revise; un proyecto recién creado con dotnet new console no lo trae.
Swift: el desbordamiento atrapa (trap), es decir, detiene el programa a propósito. El caso con constantes ni siquiera compila:
ovc.swift:1:26: error: arithmetic operation '2147483647 + 1' (on type 'Int32') results in an overflow
Con valores que solo se conocen en ejecución, el programa imprimió about to multiply y salió con estado 133 (128 + señal 5, SIGTRAP), tanto con -Onone como con -O. Si quieres que dé la vuelta, lo pides: a &+ 1 dio -2147483648 y a.addingReportingOverflow(1) dio (-2147483648, true).
Dart: depende de dónde corra. En la VM de Dart, int es de 64 bits y da la vuelta: 9223372036854775807 + 1 imprimió -9223372036854775808. Compilado a JavaScript con dart compile js, un int es un número de JS, así que el compilador rechaza los literales que no puede representar:
Error: The integer literal 9007199254740993 can't be represented exactly in JavaScript.
Cuando el valor venía de int.parse, el código compilado (ejecutado en jsc, no en un navegador) imprimió 9223372036854776000 para el máximo de 64 bits y 9007199254740992 para 2^53 + 1, y identical(1, 1.0) pasó a ser true. Mismo código fuente, números distintos: prueba Flutter web por separado.
PHP: un int que se desborda se convierte en 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
Sin error, pero el float solo tiene 53 bits de precisión, así que PHP_INT_MAX + 1 == PHP_INT_MAX es verdadero. Convertir de vuelta a int dio una advertencia en 8.5.6; no probamos si versiones anteriores la muestran. bcadd("9223372036854775807", "1") y gmp_add imprimieron el exacto 9223372036854775808.
Python: los enteros nunca se desbordan. (2**63 - 1) + 1 imprimió 9223372036854775808, y 2**64 * 2**64 imprimió los 39 dígitos. Los límites aparecen solo donde Python entrega números a un mundo de tamaño fijo: struct.pack('<i', 2**31) lanzó struct.error: 'i' format requires -2147483648 <= number <= 2147483647, y (2**64).to_bytes(8, 'little') lanzó OverflowError: int too big to convert.
JavaScript: MAX_SAFE_INTEGER e IDs que pierden dígitos
Un Number de JavaScript es un double (lección 1). Su mantisa de 53 bits guarda exacto cada entero hasta 2^53; por encima, los huecos entre doubles son de 2, luego de 4, luego de 8…
$ jsc safe.js
9007199254740991 true // Number.MAX_SAFE_INTEGER, === 2**53 - 1
9007199254740992 9007199254740992 true // 2**53 y 2**53 + 1 son iguales
true false // Number.isSafeInteger(2**53 - 1), (2**53)
9007199254740992 9007199254740992 9007199254740994 9007199254740996 9007199254740996 // 2**53 + 0..4
Donde más duele es en los IDs. Muchas bases de datos y esquemas de IDs (claves autoincrementales de 64 bits, IDs tipo Snowflake de sistemas distribuidos) producen números por encima de 2^53. El servidor manda un JSON correcto, y el JSON.parse del navegador los cambia sin avisar:
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"}
El ID cambió en 11, el ID del pedido en 1, y no apareció ningún error. Si el cliente devuelve ese ID, actualiza la fila equivocada o ninguna. El string sid sobrevivió. json.loads de Python y json_decode de PHP 8.5 leyeron bien 9007199254740993, porque sus enteros son más grandes; PHP convirtió 12345678901234567890 (más de 64 bits) en float(1.2345678901234567E+19) salvo que pases JSON_BIGINT_AS_STRING, que dio string(20) "12345678901234567890". Mira PHP prepared statements changing JSON number types (en inglés) para una trampa parecida del lado del servidor.
El arreglo, en orden de preferencia:
- Envía los IDs como strings. Tu API devuelve
"id": "1234567890123456789". Los IDs son etiquetas, no cantidades; nunca los sumas. Funciona en cualquier cliente. - Usa
BigIntpara hacer cuentas.BigInt("1234567890123456789") + 1nimprimió1234567890123456790. BigInt tiene sus propias reglas, todas revisadas enjsc:
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.
- Lee el texto original al parsear. Los motores nuevos le pasan al reviver de
JSON.parseel texto fuente de cada número, yJSON.rawJSONescribe un BigInt de vuelta como número sin comillas. Ambos funcionaron en eljscde esta Mac:
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}
Estas dos funciones llegaron hace poco al lenguaje; revisa que todos los navegadores y versiones de Node.js que soportas las tengan antes de depender de ellas (solo ejecutamos jsc). La opción 1 no tiene esa advertencia.
Otro límite silencioso de 32 bits: los operadores de bits convierten a enteros de 32 bits. (2147483647 + 1) | 0 imprimió -2147483648, y Math.imul(2000000, 1500) imprimió -1294967296. La lección de rendimiento y números en JavaScript profundiza en Number y BigInt.
Bases de datos: el INTEGER de SQLite en el límite
El INTEGER de SQLite es un valor de 64 bits con signo. En el borde hace tres cosas distintas:
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
El operador + cambia a float en silencio, sum() lanza un error y total() siempre devuelve un float. Insertar 12345678901234567890 en una columna INTEGER lo guardó como real; los mismos dígitos entre comillas también pasaron a real, porque la columna tiene afinidad entera. Guarda los valores de más de 64 bits en una columna TEXT.
Claves primarias en el límite:
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
Cuando el rowid más grande ya está ocupado, SQLite elige uno libre al azar. Con AUTOINCREMENT, que promete no reutilizar ni ir hacia atrás, la misma inserción falló: Error: stepping, database or disk is full (13). No vas a llegar a 9.2×10^18 contando, pero sí si alguien inserta un ID enorme a mano. Para lo básico de SQL, mira SELECT, WHERE y ORDER BY.
Errores comunes y qué tipo usar
| Error | Lo que pasa en realidad | Arreglo |
|---|---|---|
long long t = a * b; con int a, b (en Java, long t = a * b) |
la multiplicación se desborda primero en 32 bits: -1294967296 |
ensancha un operando: (long long)a * b |
Contar milisegundos en un int |
2^31 ms son unos 24.8 días | usa 64 bits |
Math.abs(x) para que no sea negativo |
Math.abs(Integer.MIN_VALUE) es -2147483648 |
revisa el mínimo o usa long |
| IDs numéricos en JSON para un cliente JS | 1234567890123456789 llega como 1234567890123456800 |
envía los IDs como strings |
Mezclar BigInt y Number |
TypeError: Invalid mix of BigInt and other type |
convierte explícitamente; un solo tipo por valor |
| Confiar en la aritmética int de PHP pasado 2^63 | se vuelve float en silencio, == PHP_INT_MAX es verdadero |
bcmath o gmp |
| Un mismo código Dart para VM y web | 9223372036854775807 se vuelve 9223372036854776000 en web |
mantén los valores bajo 2^53, o BigInt |
Cómo elegir tipo:
| Vas a guardar | Usa | Evita |
|---|---|---|
| Conteos, cantidades, unidades mínimas de moneda bajo unos 9×10^15 | entero de 64 bits (long, Int, int en PHP/Dart VM, INTEGER) |
int de 32 bits si los totales pueden crecer |
| IDs de una base de datos u otro servicio | string en JSON y en JS; entero de 64 bits en el servidor | Number de JS |
| Enteros con los que debes operar más allá de 2^63 | BigInteger, int de Python, BigInt, gmp |
floats |
| Valores que nunca deben desbordarse en silencio | *Exact de Java, checked de C#, operadores normales de Swift, __builtin_*_overflow |
el + simple en C/Java/C# |
El dinero en sí, con centavos y tipos decimales, es la lección 4. Para lo básico de tipos, mira tipos en Java y tipos en PHP.
Medido: cuántos IDs sobreviven a JSON.parse y cuánto cuesta BigInt
¿Cuántos IDs pasan intactos por JSON.parse? Generamos 100,000 enteros aleatorios en cada uno de cinco rangos con Python (random.seed(7)), los escribimos como números JSON y como strings, parseamos ambos en jsc y comparamos cada número parseado con su string usando BigInt.
| Rango | Sobrevivieron | Error máximo |
|---|---|---|
| hasta 2^53 - 1 | 100,000 (100%) | 0 |
| 2^53 a 2^54 | 49,994 (49.99%) | 1 |
| 2^54 a 2^56 | 16,718 (16.72%) | 4 |
| 2^59 a 2^61 (tamaño Snowflake) | 526 (0.53%) | 128 |
| 10^18 a 2^63 (19 dígitos) | 189 (0.19%) | 512 |
Por encima de 2^53 solo sobreviven los IDs que por casualidad son múltiplos del tamaño del hueco. De los IDs de 19 dígitos, el 99.8% sale mal, con errores de hasta 512.
¿Cuánto cuesta BigInt? Sumar de 0 a 9,999,999 en jsc, mediana de 7 ejecuciones en una Mac con Apple Silicon (M1 Pro): Number 3.2 ms, BigInt 301.3 ms, los dos con resultado 49999995000000. El JIT optimiza muy bien el bucle con Number, así que toma la diferencia de unas 94 veces como cota superior, no como constante. El int de tamaño arbitrario de Python, sumando 1,000,000 de valores a un total (mediana de 7):
| Valor inicial | 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 |
Los enteros grandes no son gratis, pero hasta unos cientos de bits Python apenas lo nota. Usa BigInt donde lo necesites, no en todas partes.
Límites honestos y notas de versión
- Los tamaños de enteros de C son los de clang en macOS arm64. En Windows de 64 bits
longes de 32 bits (no se ejecutó aquí). - Los resultados del desbordamiento con signo en C son lo que hizo este compilador con
-O0; el estándar no promete nada. - Swift no imprimió ningún mensaje para el trap en la salida que capturamos, solo el estado de salida 133; el depurador de Xcode muestra más (no se ejecutó aquí).
- El acceso al texto fuente en
JSON.parseyJSON.rawJSONse revisaron solo en eljscde esta Mac. El límite de 4,300 dígitos de Python: 3.12.14 lanzóValueError: Exceeds the limit (4300 digits) for integer string conversiony 3.9.6 imprimió 5,001 dígitos, porque el límite llegó en 3.9.14. - La salida de Dart compilada a JavaScript se ejecutó en
jsc, no en un navegador. Node.js no se ejecutó. - Los tiempos son de una sola máquina; compara proporciones.
Ejercicios
1. En Java, ¿cuáles imprimen 3000000000?
int price = 2_000_000, qty = 1500;
long a = price * qty;
long b = (long) price * qty;
long c = (long) (price * qty);
2. Tu API devuelve {"id": 9007199254740993} y el cliente web manda ese ID de vuelta para borrar un registro. ¿En qué convierte jsc el ID y qué debería cambiar la API?
3. En Python, x = 2**31 no da problemas. Nombra un lugar donde el mismo valor lanza un error y cita el error.
Respuestas
1. Solo b. Nuestra ejecución imprimió -1294967296 3000000000 -1294967296 para a b c. a y c multiplican primero en 32 bits; el cast de c llega después del desbordamiento.
2. JSON.parse dio 9007199254740992, uno menos, así que el cliente intentaría borrar el registro ...992, no el ...993. Devuelve el ID como string, "id": "9007199254740993", y mantenlo como string en el cliente.
3. struct.pack('<i', 2**31) lanzó struct.error: 'i' format requires -2147483648 <= number <= 2147483647. Lo mismo pasa al escribir un formato de archivo binario, una biblioteca de C o una columna de base de datos de 32 bits.
Lo que sigue
La lección 3 trata del redondeo: por qué round(2.5) en Python da 2, qué hace en realidad toFixed y cómo el IVA por línea y el IVA sobre el total dan resultados distintos.
- Anterior: curso de números y dinero 1/6: por qué 0.1 + 0.2 no da 0.3
- Siguiente: curso de números y dinero 3/6: modos de redondeo, redondeo bancario e IVA
- Más adelante: dinero en centavos enteros y decimales, formato y lectura de números según la región, error de suma y NaN
- Dónde encaja esto: ruta de aprendizaje de programación
Preguntas frecuentes
¿Por qué JSON.parse cambia los últimos dígitos de mi ID?
Los números de JavaScript son doubles, que guardan todos los enteros exactos solo hasta 2^53 - 1 (Number.MAX_SAFE_INTEGER, 9007199254740991). Más allá, el parser elige el double más cercano. En jsc, JSON.parse convirtió 1234567890123456789 en 1234567890123456800 y 9007199254740993 en 9007199254740992. Envía los IDs grandes como strings en el JSON y conviértelos con BigInt solo si necesitas hacer cuentas.
¿Qué pasa cuando un int se desborda en Java o C#?
Por defecto los dos dan la vuelta en silencio: Integer.MAX_VALUE + 1 da -2147483648 y 2,000,000 × 1,500 da -1294967296. Math.addExact y Math.multiplyExact de Java lanzan ArithmeticException: integer overflow; C# lanza OverflowException dentro de checked(...) o con CheckForOverflowUnderflow activado.
¿Los enteros de Python se desbordan?
No. El int de Python crece lo que haga falta: (2**63 - 1) + 1 imprimió 9223372036854775808. Los límites aparecen en los bordes: struct.pack('<i', 2**31) lanza struct.error, y desde las versiones de seguridad de 2022 (3.11, 3.10.7, 3.9.14 y posteriores) convertir a texto un int de más de 4,300 dígitos lanza ValueError por defecto.