Punto flotante de juguete
Matemáticamente, si tuviéramos infinita precisión, x vale 0, 1-x da 1 y a otra cosa. Pero si hacemos las cuentas con números en punto flotante todo se complica. Para empezar si x es distinto de 0 pero muy chiquito, 1+x y 1-x también valen 1.Casi todas las computadoras actuales unan la norma IEEE, pero es muy complicada. Para entender lo que está pasando vamos a usar una versión de juguete sin números desnormalizados, ni infinitos, ni NANs. Pero tratando que se comporte igual en la parte que nos interesa.
La versión de juguete usa:
S|EXP|MANTISA
- S es para el signo: usamos un solo bit, un 0 indica positivo y 1 negativo, mucho no importa en lo que sigue, así que vamos a escribirlo como + ó – directamente.
- EXP guarda el exponente: La idea es que el número es ALGO*2EXP. Como todo esto es en binario, usamos ALGO*2EXP en vez de ALGO*10EXP. Si EXP es positivo es un número grande, si EXP es negativo es una fracción. Ya que es una versión de juguete vamos a usar magia y suponer que EXP puede ser cualquier número entero (En la versión real, EXP sólo toma valores en cierto rango y también sirve para identificar los infinitos y NAN. [*])
- MANTISA: En la versión de juguete vamos a suponer que tiene 4 bits ABCD. No es muy importante que sean exactamente 4 bits, pero es más fácil hacer las cuentas con 4 bits que con los veintipico de la versión real.
Si normalizamos a un número lo multiplicamos o dividimos por 2 hasta que esté entre 1 (inclusive) y 2 (excluido). Al escribirlo en base 2 nos queda que la parte entera es 1 y el resto son decimales. Entonces podemos no escribir el primer 1 que está siempre ahí y a la mantisa ABCD la interpretamos como el número 1,ABCD2.
Unos ejemplos
Juntando todo esto, el número S|EXP|ABCD esX = S 2EXP 1,ABCD2
Por ejemplo +|-3|1001 representa el número
X = + 2-3 1,10012 = + 1/8 (1 + 1 1/2 + 0 1/4 + 0 1/8 + 1 1/16) = 1/8 * 25/16 = 25/128 = 0,1953125
Otro ejemplo -|2|0011 representa el número
X = - 22 1,00112 = - 4 (1 + 0 1/2 + 0 1/4 + 1 1/8 + 1 1/16) = - 4 * 19/16 = - 19/4 = - 4,75
El lector astuto habrá notado que al normalizar el número para que esté entre 1 y 2, el número no puede ser 0. Pensado de otra forma, ya que agregamos el 1 a ABCD, no hay forma de escribir el 0. ¡En la versión de juguete no hay 0! (En la versión real esto también se anota en EXP, y hay que agregar este caso en [*].) Para lo que vamos a hacer no es tan malo que no haya 0, así que sigamos adelante.
Los números más cercanos a 1
Con esta notación no podemos escribir todos los números reales, solo alguna fracciones que tienen en el denominador una potencia de 2 (esto incluye a los enteros).Por ejemplo el número 1 lo podemos escribir como +|0|0000 porque
X = + 20 1,00002 = + 1 (1 + 0 1/2 + 0 1/4 + 0 1/8 + 0 1/16) = 1 * 16/16 = 1
(Es extraño, pero todos ceros no representa al 0 sino al 1.)
El siguiente número que podemos escribir, subiendo lo menos posible, es +|0|0001 que es
X = + 20 1,00012 = + 1 (1 + 0 1/2 + 0 1/4 + 0 1/8 + 1 1/16) = 1 * 17/16 = 1,0625 = 1 + 0,0625
No podemos escribir ningún número entre 1 y 1,0625, porque hay saltitos. (En la notación IEEE la mantisa tiene más bits y los saltitos son mucho más chiquitos, pero igual hay saltitos.)
Para ir hacia abajo es más difícil. Los números de la forma +|0|ABCD están entre 1(inclusive) y 2(excluido). Así que para agarrar un número más chico tenemos que bajar el exponente, y ver los números +|-1|ABCD. El más grande de ellos es claramente +|-1|1111 que representa el número
X = + 2-1 1,11112 = + 1/2 (1 + 1 1/2 + 1 1/4 + 1 1/8 + 1 1/16) = 1/2 * 31/16 = 31/32 = 0,96875 = 1 – 0,03125
O sea que del 1 para arriba hay un salto de 0,0625 = 1/16 y del 1 para abajo hay un salto de 0,03125 = 1/32. Los saltos de números que no se pueden escribir dependen del rango en que uno esté trabajando y por ejemplo cambian justo en 1.
Si 1+x vale 1, ¿cuánto vale x?
En realidad nos interesa sólo si x es positivo. Si es negativo la idea es equivalente.Para que 1+x no sea 1, al menos tiene que ser 1+1/16, pero al sumar en esta representación se redondea al número más cercano representable. A la mitad entre 1 y 1+1/16 está 1+1/32. Los resultados menores a 1+1/32 se redondean a 1 y los mayores a 1+1/32 se redondean a 1+1/16. Si el resultado es justo 1+1/32, depende del redondeo, supongo que se utiliza el criterio de elegir para el lado par, así que se debe redondear a 1. Con esto queda para que 1+x valga 1, x tiene que estar entre 0 y casi 1/32. (El caso 1/32 es dudoso, habría que leer todo el estandar para estar seguros.)
Si 1-x vale 1, ¿cuánto vale x?
Para que 1-x no sea 1, el resultado tiene que ser como máximo 1-1/32, después de redondear. A la mitad entre 1 y 1-1/32 está 1-1/64. Los resultados mayores a 1-1/64 se redondean a 1 y los menores a 1-1/64 se redondean a 1-1/32. Si el resultado es justo 1-1/64, depende de los detalles del redondeo, supongo que se utiliza el criterio de elegir para el lado par, así que se debe redondear a 1. Con esto queda para que 1-x valga 1, x tiene que estar entre 0 y casi 1/64. (El caso 1/64 es dudoso, si ya hubiéramos leído todo el estándar sabríamos que pasa.)Si 1+x vale 1, ¿cuánto vale 1-x?
Lo importante es que el rango de x (positivos) en que 1+x es 1 es distinto del rango de x en que 1-x es 1, y eso nos va a traer resultados inesperados.x | 1+x | 1-x |
---|---|---|
0<x<1/64 | 1 | 1 |
x=1/64 | 1 | ¿1? |
1/64<x<1/32 | 1 | 1-1/32 |
x=1/32 | ¿1? | 1-1/32 |
1/32<x | 1+1/16 o más | 1-1/32 o menos |
O sea que los casos interesantes son cuándo x está entre 1/64 y 1/32 (y quizás los bordes). Con notación exponencial, esto es cuándo x está entre 1/26 y 1/25. Con un poco de optimismo, podemos escribir esto como 1/24+2 y 1/24+1, pensando que la mantisa tiene 4 bits. Mirando con cuidado las cuentas que hicimos, podemos transformar esto en una demostración y reescribirlo como 1/2M+2 y 1/2M+1, en donde M es la cantidad de bits que escribimos explícitamente en la mantisa.
Si 1+x vale 1, ¿cuánto vale 1-x? (IEEE)
Usando el estandard IEEE en vez de la versión de juguete, la diferencia más importante es que en vez de tener sólo 4 bits de mantisa tenemos 24 bits para los single (32 bits contando los que se usan para guardar el signo y el exponente) y 52 bits para los double (64 bits contando los que se usan para guardar el signo y el exponente).El exponente ya no es mágico, y hay que ver bien que pasa con los NAN, infinitos y cero, pero por suerte estos cambios no afectan lo que queremos ver. Suponiendo que el lector habrá hecho la demostración, siguiendo los pasos anteriores tenemos que para que 1+x sea 1 y 1-x sea distinto de 1 x tiene que estar en el siguiente rango
Estándar | Total | Mantisa | Xmin | Xmax |
---|---|---|---|---|
Juguete | ∞ :) | 4 | 1/26 = 1/4 1/24 = 0.0156 | 1/25 = 1/2 1/24 = 0,0312 |
Single | 32 | 23 | 1/225 = 1/4 1/223 = 2,98*10-8 | 1/224 = 1/2 1/223 = 5,96*10-08 |
Double | 64 | 52 | 1/254 = 1/4 1/252 = 5,55*10-17 | 1/253 = 1/2 1/252 = 1,11*10-16 |
Experimentalmente
Veamos como anda esto experimentalmente. Probemos con el siguiente programa en Racket. Primero hacemos una función genérica que toma valores de x entre 0 y 1/2bits, partidos en 2extra pasitos. Después la probamos con números de doble precisión (como 1.0) y de simple precisión (como 1.f0).#lang racket
(define (almost-one bits extra one)
(for ([i (in-range (add1 (expt 2 extra)))])
(define x (/ i (expt 2 (+ bits extra))))
(displayln (format "~a/2^~a ~a ~a ~a"
(~r (* x (expt 2 bits)) #:precision '(= 4))
bits
(= (+ one x) one)
(= (- one x) one)
(~r x #:notation 'exponential)))))
(almost-one 52 4 1.0)
(newline)
(almost-one 23 4 1.f0)
Los resultados son
Double precision (32 bits) | Single precision (16 bits) |
---|---|
(almost-one 52 4 1.0) | (almost-one 23 4 1.f0) |
0.0000/2^52 #t #t 0e+00 0.0625/2^52 #t #t 1.387779e-17 0.1250/2^52 #t #t 2.775558e-17 0.1875/2^52 #t #t 4.163336e-17 0.2500/2^52 #t #t 5.551115e-17 0.3125/2^52 #t #f 6.938894e-17 0.3750/2^52 #t #f 8.326673e-17 0.4375/2^52 #t #f 9.714451e-17 0.5000/2^52 #t #f 1.110223e-16 0.5625/2^52 #f #f 1.249001e-16 0.6250/2^52 #f #f 1.387779e-16 0.6875/2^52 #f #f 1.526557e-16 0.7500/2^52 #f #f 1.665335e-16 0.8125/2^52 #f #f 1.804112e-16 0.8750/2^52 #f #f 1.94289e-16 0.9375/2^52 #f #f 2.081668e-16 1.0000/2^52 #f #f 2.220446e-16 |
0.0000/2^23 #t #t 0e+00 0.0625/2^23 #t #t 7.450581e-09 0.1250/2^23 #t #t 1.490116e-08 0.1875/2^23 #t #t 2.235174e-08 0.2500/2^23 #t #t 2.980232e-08 0.3125/2^23 #t #f 3.72529e-08 0.3750/2^23 #t #f 4.470348e-08 0.4375/2^23 #t #f 5.215406e-08 0.5000/2^23 #t #f 5.960464e-08 0.5625/2^23 #f #f 6.705523e-08 0.6250/2^23 #f #f 7.450581e-08 0.6875/2^23 #f #f 8.195639e-08 0.7500/2^23 #f #f 8.940697e-08 0.8125/2^23 #f #f 9.685755e-08 0.8750/2^23 #f #f 1.043081e-07 0.9375/2^23 #f #f 1.117587e-07 1.0000/2^23 #f #f 1.192093e-07 |
Si subimos el valor de extra, vemos que el máximo siempre está en 1/2 y el mínimo se acerca cada vez más a 1/4, como habíamos marcado en la otra tabla.
Para pensar
- ¿Qué pasa con (almost-one 52 4 0.5)?
- ¿Qué pasa con (almost-one 52 4 1.5)?
- ¿Qué pasa con (almost-one 52 4 1)?