Saltar al contenido

Desmitificando el punto flotante: 0.20000000000000001110

Esta es la parte 2 de una serie en la que hablo sobre el punto flotante.

Empezamos con un ejemplo para ver cómo transformar un número real decimal en su representación binaria de 32 bits.

Imagina que tenemos el número real 11.75. ¿Cómo puedo representar esto?

Real11.75
\(1·2^3+0·2^2+1·2^1+1·2^0\).\(1·2^{-1}+1·2^{-2}\)
Binario1011.11

En lo anterior se ha representado la parte entera (once) como binario y la parte decimal (75) como binario. Se ha cambiado de base, de decimal a binaria.

Ahora hay que hacer una cosa que se llama normalizar el binario. Normalizar en este contexto es hacer que el bit más significativo sea 1 “moviendo la mantisa” hacia la derecha o hacia la izquierda. Esto significa que, en este caso, hemos de mover el decimal hacia la izquierda hasta que solo quede un 1 delante del decimal. Lo que tenemos ahora es:

$$1011.11 x 2^0$$

Cada posición que movamos el decimal hacia la derecha es un valor que hemos de incrementar al exponente del 2 que hemos colocado a la derecha.

Punto de partida\(1011.11 x 2^0\)
\(101.111 x 2^1\)
\(10.1111 x 2^2\)
Punto final\(1.01111 x 2^3\)

El número final anterior se parece cada vez más a:

$$signo \space b^{exponente} \space mantisa$$

En este caso, el signo es positivo por lo que el bit que representa al número es 0. Recuerda \((-1)^0\).

En el caso de \(b^{exponente}\) tenemos que \(b=2\) y que \(exponente=3\). De ahí podemos deducir que \(exponente=3+127=130\) que en binario es 10000010 (\(1·2^7+0·2^6+0·2^5+0·2^4+0·2^3+0·2^2+1·2^1+0·2^0\)).

Por último, a la mantisa se le añade el valor de “1.” por defecto delante del mismo pero no se cuenta en lo que almacenamos como la parte binaria. Por tanto, la mantisa que nos queda, después del “1.”, es 01111, pero en un número de 32 bits tenemos 23 dígitos para la mantisa y, rellenando, sería, por tanto, 01111000000000000000000.

Por tanto, nuestro número 11.75 original codificado en binario de 32 bits sería:

SignoExponenteMantisa
1 bit8 bits23 bits
01000001001111000000000000000000

Punto flotante desde el punto de vista de Fabien Sanglard

Lo de antes es la explicación técnica pero hay una forma de verlo que me parece mucho más intuitiva y visual. Es la que expone Fabien Sanglard en este artículo en su página web.

Básicamente, en lugar de ver el número dividido en las partes de signo, exponente y mantisa lo que hace es renombrar lo anterior como signo, ventana y offset. El offset lo podríamos traducir como compensación pero por simplificar lo voy a dejar como offset de aquí en adelante.

El signo lo podemos ver que funciona de la misma forma que hemos definido antes. La ventana lo podemos ver como cubos de canicas donde podemos meter tantas canicas como nos permita el offset.

Vale, vamos por partes. Imaginad los cubos que he comentado antes como lo siguiente (pensando en un float de 32 bits):

Cada cubo se define como lo definido entre \(2^n\) y \(2^{n+1}\). Volviendo al número que hemos usado en el ejemplo de más arriba, 11.75. El valor del 11 estaría entre 8 y 16 que es \(2^3\) y \(2^4\).

En mi cubo caben bolitas pero, ¿cuantas bolitas caben? Pues eso lo define la mantisa. Para la mantisa, en un float de 32 bits, teníamos que podíamos usar 23 bits. El número de bolitas que caben en el cubo serán \(2^{23}=8388608\). Es decir, podemos dividir el rango entre 8 y 16 en más de 8 millones de partes. Cada bolita sería una de esas partes.

Entre 8 y 16 tendremos que cada bolita/división vale \(\frac{16-8}{8388608}=0.00000095367431\). ¿Qué pasa si nos vamos a otros cubos? Como en cada cubo cabrá el mismo número de bolitas tenemos que el valor de cada bolita será mayor o menor dependiendo del cubo en el que caigan (y esto afecta a la precisión).

Por ejemplo, el valor de la bolita para algunos de los cubos, desde \(2^{-10}\) hasta \(2^{10}\):

print('...')
for n in range(-10,10):
    print(f'n={n:3}, n+1={n+1:3}, '
          f'2^n={2**n:12}, 2^(n+1)={2**(n+1):12}, '
          f'tamaño bolita={(2**(n+1)-2**n)/2**23}')
print('...')

Lo anterior mostrará:

...
n=-10, n+1= -9, 2^n=0.0009765625, 2^(n+1)= 0.001953125, tamaño bolita=1.1641532182693481e-10
n= -9, n+1= -8, 2^n= 0.001953125, 2^(n+1)=  0.00390625, tamaño bolita=2.3283064365386963e-10
n= -8, n+1= -7, 2^n=  0.00390625, 2^(n+1)=   0.0078125, tamaño bolita=4.656612873077393e-10
n= -7, n+1= -6, 2^n=   0.0078125, 2^(n+1)=    0.015625, tamaño bolita=9.313225746154785e-10
n= -6, n+1= -5, 2^n=    0.015625, 2^(n+1)=     0.03125, tamaño bolita=1.862645149230957e-09
n= -5, n+1= -4, 2^n=     0.03125, 2^(n+1)=      0.0625, tamaño bolita=3.725290298461914e-09
n= -4, n+1= -3, 2^n=      0.0625, 2^(n+1)=       0.125, tamaño bolita=7.450580596923828e-09
n= -3, n+1= -2, 2^n=       0.125, 2^(n+1)=        0.25, tamaño bolita=1.4901161193847656e-08
n= -2, n+1= -1, 2^n=        0.25, 2^(n+1)=         0.5, tamaño bolita=2.9802322387695312e-08
n= -1, n+1=  0, 2^n=         0.5, 2^(n+1)=           1, tamaño bolita=5.960464477539063e-08
n=  0, n+1=  1, 2^n=           1, 2^(n+1)=           2, tamaño bolita=1.1920928955078125e-07
n=  1, n+1=  2, 2^n=           2, 2^(n+1)=           4, tamaño bolita=2.384185791015625e-07
n=  2, n+1=  3, 2^n=           4, 2^(n+1)=           8, tamaño bolita=4.76837158203125e-07
n=  3, n+1=  4, 2^n=           8, 2^(n+1)=          16, tamaño bolita=9.5367431640625e-07
n=  4, n+1=  5, 2^n=          16, 2^(n+1)=          32, tamaño bolita=1.9073486328125e-06
n=  5, n+1=  6, 2^n=          32, 2^(n+1)=          64, tamaño bolita=3.814697265625e-06
n=  6, n+1=  7, 2^n=          64, 2^(n+1)=         128, tamaño bolita=7.62939453125e-06
n=  7, n+1=  8, 2^n=         128, 2^(n+1)=         256, tamaño bolita=1.52587890625e-05
n=  8, n+1=  9, 2^n=         256, 2^(n+1)=         512, tamaño bolita=3.0517578125e-05
n=  9, n+1= 10, 2^n=         512, 2^(n+1)=        1024, tamaño bolita=6.103515625e-05
...

Volviendo nuevamente al número del ejemplo, 11.75, podemos ver cuantas bolitas hemos de meter en el cubo de la siguiente forma: \(\frac{11.75 – 8}{16 – 8} = 0.46875\). Ese valor está cerca de la mitad de lo que le cabe al cubo. Si queremos conocer el offset lo podemos calcular así (nuevamente, es un float de 32 bits por lo que tenemos 23 bits para la mantisa): \(2^{23} * 0.46875 = 3932160\). Vamos a convertir este número en la notación IEEE754:

signo = 0
exponente = 130 # 2^(E-127) = 2^(130 - 127) = 2^3 = 8 (la parte baja del cubo o cubo vacio)
mantisa = 3_932_160

En binario:

print(f'signo={signo:b}, exponente={exponente:08b}, mantisa={mantisa:023b}')

Cuyo resultado en pantalla será:

signo=0, exponente=10000010, mantisa=01111000000000000000000

Esta segunda forma ‘copiada’ de Sanglard me resulta mucho más intuitiva de entender. Lo dejamos aquí por hoy pero…

(continuará…)

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

71 − = sixty six

Pybonacci