Atlasingeniería

Arquitectura y sistemas operativosRepresentación y lógica digitalTema 2

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

(1)s×1.m×2e1023.(-1)^{s} \times 1{.}m \times 2^{e-1023}.

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í

Sumás 0,1 más 0,2 y comparás con 0,3 usando igualdad. ¿Da verdadero?

Por qué 0,1 no existe en binario

El problema de 0.10{.}1 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.

1 / 5
Ésta es la consecuencia práctica de que la precisión sea relativa. Cerca de 1 los representables están a 2 diezmilbillonésimas; arriba de 2⁵³ el siguiente número está a 2 de distancia, así que sumarle 1 a un número grande puede no cambiarlo, y no hay error ni aviso.

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 0-0 y +0+0 son iguales al comparar pero se distinguen al dividir.

Y NaN, que representa un resultado indefinido como 0/00/0 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 10910^{-9} 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 2532^{53}.

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 preguntarlo

El 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

ParaUsarPor qué
Dineroentero de centavos o decimal exactolos errores se acumulan y son auditables
Identificadores grandestexto o entero de 64 bitsarriba de 2⁵³ los flotantes no distinguen
Medidas físicasflotanteya vienen con error de medición
Probabilidadesflotante, en escala logarítmica si son chicasmultiplicar muchas da cero por desbordamiento
Compararnunca con igualdadcon tolerancia, elegida según el dominio
La segunda fila es la que rompe sistemas reales sin que nadie lo vea venir: un identificador de 19 dígitos que pasa por JSON en un navegador pierde precisión y termina apuntando a otro registro.

Cierre

Signo, exponente y mantisa dan precisión relativa: mucha cerca del cero, poca lejos. Los decimales periódicos en base dos explican el 0.1+0.20{.}1+0{.}2; 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?

¿Por qué 0.1 + 0.2 no da exactamente 0.3?
¿Qué significa que la precisión del punto flotante sea relativa?
¿Por qué el uno inicial de la mantisa no se guarda?
¿Dónde no hay que usar punto flotante?