Punto flotante IEEE 754
Por qué 0.1 más 0.2 no da 0.3, qué significa realmente NaN y en qué casos comparar dos flotantes con igualdad es una decisión defendible.
Para este tema conviene tener claro:Binario, hexadecimal y complemento a dos
0.1 + 0.2 da 0.30000000000000004 en casi todos los lenguajes. No es un bug del lenguaje ni del
procesador: es la consecuencia directa de representar números decimales en base dos con una
cantidad fija de bits.
Notación científica en binario
Un flotante es notación científica en binario: signo, exponente y mantisa. En doble precisión son 1, 11 y 52 bits, y el valor es
El uno inicial de la mantisa no se guarda: se sabe que está. El exponente lleva un sesgo para poder representar potencias negativas sin un signo aparte. La consecuencia central es que la precisión es relativa: hay muchísimos valores representables cerca del cero y muy pocos en los números grandes.
Antes de seguir, predecí
Por qué 0,1 no existe en binario
El problema de es que en base dos es periódico, igual que un tercio en base diez. No se puede escribir exacto con ninguna cantidad finita de bits, así que se guarda el representable más cercano.
Entre 1 y 2, los flotantes de doble precisión están separados por 2 elevado a −52. Son densísimos.
Sumar dos aproximaciones da una aproximación, y al mostrarla con suficientes dígitos aparece la diferencia. Nada se rompió: el número nunca estuvo ahí. Por eso el dinero no va en flotante, sino en enteros de centavos o en un tipo decimal.
Infinitos, ceros con signo y NaN
El estándar reserva valores. El infinito positivo y negativo, que es lo que da dividir por cero en flotante en vez de una excepción. El cero con signo, donde y son iguales al comparar pero se distinguen al dividir.
Y NaN, que representa un resultado indefinido como o la raíz de un negativo. Tiene una propiedad que sorprende y es deliberada: no es igual a nada, ni siquiera a sí mismo. Ese es el truco estándar para detectarlo, y también el motivo por el que un NaN suelto en un arreglo rompe cualquier ordenamiento.
Comparar con tolerancia, y su letra chica
De ahí la regla conocida: no comparar flotantes con igualdad, sino verificar que la diferencia sea menor a una tolerancia. Con la letra chica de que la tolerancia tiene que ser relativa a la magnitud, porque un error de es enorme cerca del cero e invisible en los millones.
La excepción legítima: comparar por igualdad está bien cuando los valores vienen de asignaciones exactas y no de aritmética, como comparar contra cero un contador que sólo se asignó.
Los errores se acumulan, y no de forma inocente
Los errores se acumulan y no lo hacen de forma inocente. Sumar un número chico a uno muy grande puede no cambiar nada: el chico cae fuera de la precisión disponible y se pierde entero.
Por eso sumar un arreglo de menor a mayor da un resultado más preciso que al revés, y existe la suma de Kahan, que lleva un término de corrección. Y por eso la aritmética de flotantes no es asociativa: cambiar el orden de las operaciones cambia el resultado, lo que explica que un cálculo paralelizado no dé bit a bit lo mismo que el secuencial.
Cuántos dígitos hay, en serio
La precisión simple tiene unos 7 dígitos decimales; la doble, unos 15. Eso importa cuando se manejan identificadores grandes: JavaScript usa doble para todos sus números, y los enteros dejan de ser exactos más allá de .
Es la razón concreta por la que un identificador de 64 bits que viaja como número en JSON puede llegar alterado, y por la que las APIs serias los mandan como cadena.
Los tres casos que hay que conocer
0.1 + 0.2 === 0.3; // false
Math.abs(0.1 + 0.2 - 0.3) < 1e-9; // así se compara
// La precisión se gasta con el orden de magnitud
Number.MAX_SAFE_INTEGER; // 9007199254740991, o sea 2^53 - 1
9007199254740993 === 9007199254740992; // true: ya no se distinguen
// Sumar muchos números chicos a uno grande los pierde
let total = 1e9;
for (let i = 0; i < 1_000_000; i += 1) total += 0.000001;
total; // sigue siendo 1000000000, no 1000000001
// Los valores especiales
0 / 0; // NaN, y NaN !== NaN
1 / 0; // Infinity
Number.isNaN(0 / 0); // la forma correcta de preguntarloEl tercer caso es el que más sorprende y el que más aparece en cálculos reales: sumar un millón de valores muy chicos a un acumulador grande no cambia nada, porque cada suma individual cae por debajo de la precisión disponible en esa magnitud.
La solución cuando importa es sumar en orden creciente, o usar el algoritmo de Kahan, que arrastra el error de redondeo en una variable aparte y lo reinyecta.
Cuándo flotante y cuándo no
| Para | Usar | Por qué |
|---|---|---|
| Dinero | entero de centavos o decimal exacto | los errores se acumulan y son auditables |
| Identificadores grandes | texto o entero de 64 bits | arriba de 2⁵³ los flotantes no distinguen |
| Medidas físicas | flotante | ya vienen con error de medición |
| Probabilidades | flotante, en escala logarítmica si son chicas | multiplicar muchas da cero por desbordamiento |
| Comparar | nunca con igualdad | con tolerancia, elegida según el dominio |
Cierre
Signo, exponente y mantisa dan precisión relativa: mucha cerca del cero, poca lejos. Los decimales periódicos en base dos explican el ; NaN no es igual a sí mismo; la suma no es asociativa; y el dinero y los identificadores grandes no van en flotante.
Autoevaluación
¿Lo entendiste?
Práctica