Desmitificando el punto flotante: 0.30000000000000004440

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

Parto desde donde lo dejé en el anterior capítulo donde usábamos la forma de Fabien Sanglard de entender los números de punto flotante para obtener el valor de 11.75. Continuamos…

Usando esta misma deducción de Fabien Sanglard para el número 0.1, que ya hemos visto desde un principio que no se puede representar de forma exacta usando esta formulación, tenemos:

  • Signo es positivo, \(signo = 0\)
  • Cubo entre \(n=-4\) y \(n=-3\),
    • \(2^{-4} = 0.0625\) y \(2^{-3} = 0.125\),
    • \(Exponente – 127 = -4\), \(Exponente = 123\)
  • Tamaño bolita: \(\frac{0.125 – 0.0625}{2^{23}}=7.450580596923828e-09\)
    • Cantidad de bolitas en el cubo en tanto por uno: \(\frac{0.1 – 0.0625}{0.125 – 0.0625}=0.6\)
    • Mantisa: \(2^{23}*0.6 = 5033164.8\)

Ups, veis que la mantisa no da un número entero. Vamos a redondear la mantisa y a representarlo en binario:

El anterior código debería mostrar:

El número anterior es el más cercano a 0.1 que podemos representar usando esta formulación. El error que cometemos será:

  • La parte que no nos cubre la mantisa, \(5033165 – 5033164.8 = 0.2\)
  • que, multiplicado por el tamaño de la bolita, \(7.450580596923828𝑒−09\)
    • nos dará como resultado un error de: \(1.4901161193847657e-09\)

Llegados a este punto, si necesitamos una precisión mayor que la que podemos obtener viendo el error que estamos cometiendo nos tendríamos que plantear usar float‘s de 64 bits que nos darán mayor precisión (y también nos ocuparán más memoria).

Lo mismo para 0.2 sería:

  • Signo es positivo, \(signo = 0\)
  • Cubo entre \(n=-3\) y \(n=-2\),
    • \(2^{-3} = 0.125\) y \(2^{-2} = 0.25\),
    • \(Exponente – 127 = -3\), \(Exponente = 124\)
  • Tamaño bolita: \(\frac{0.25 – 0.125}{2^{23}}=1.4901161193847656e-08\)
    • Cantidad de bolitas en el cubo en tanto por uno: \(\frac{0.2 – 0.125}{0.25 – 0.125}=0.6\)
    • Mantisa: \(2^{23}*0.6 = 5033164.8\)

Nuevamente, no tenemos una mantisa entera. Por tanto, si redondeamos, el número más cercano que podemos obtener y su error será:

Lo anterior debería mostrar en pantalla:

Repito lo anterior para un float de 64 bits

[Lo siguiente solo es para mostrar mediante un ejemplo cómo sería usando un float de 64 bits].

Recordamos que en un float de 64 bits tenemos 11 bits reservados para el exponente, 52 para la mantisa y uno para el signo.

Para un número de 64 bits en el que tenemos 11 bits para el exponente tenemos \(Emax = 2^{(11-1)} – 1 = 1023\) y \(Emin = 1 – Emax = 1 – 1023 = -1022\).

¿Cómo sería la representación en 64 bits para el número 0.1?:

  • Signo es positivo, \(signo=0\)
  • Cubo entre \(𝑛=−4\) y \(𝑛=−3\),
    • \(2^{-4}=0.0625\) y \(2^{-3}=0.125\),
    • \(Exponente−1023=−4\), \(Exponente=1019\)
  • Tamaño bolita: \(\frac{0.125−0.0625}{2^{52}}=1.3877787807814457e-17\)
    • Cantidad de bolitas en el cubo en tanto por uno: \(\frac{0.1−0.0625}{0.125−0.0625}=0.6\)
    • Mantisa: \(2^{52}∗0.6=2702159776422297.5\)

El resultado de lo anterior sería:

¿Cómo sería la representación en 64 bits para el número 0.2?

  • Signo es positivo, \(signo=0\)
  • Cubo entre \(𝑛=−3\) y \(𝑛=−2\),
    • \(2^{-3}=0.125\) y \(2^{-2}=0.25\),
    • \(Exponente−1023=−3\), \(Exponente=1020\)
  • Tamaño bolita: \(\frac{0.25−0.125}{2^{52}}=2.7755575615628914e-17\)
    • Cantidad de bolitas en el cubo en tanto por uno: \(\frac{0.2−0.125}{0.25−0.125}=0.6\)
    • Mantisa: \(2^{52}∗0.6=2702159776422297.5\)

El resultado de lo anterior será:

Hasta aquí, espero que hayáis entendido, más o menos, cómo se representan los números en nuestro PC. Pero, y ¿las operaciones aritméticas básicas como una suma o una resta?

¿Cómo se hacen las operaciones aritméticas, por ejemplo, la suma?

Voy a escribir en seudocódigo el algoritmo (la suma y resta es lo mismo, solo depende de los signos de los números envueltos en el cálculo):

Tenemos dos números, \(n_1\) y \(n_2\) y queremos obtener la suma de ambos, \(n_3 = n_1 + n_2\). El proceso sería cómo sigue:

  • \(n_1\) y \(n_2\) solo se pueden sumar si tienen el mismo exponente.
  • El valor absoluto de \(n_1\) tiene que ser mayor que el valor absoluto de \(n_2\). Si esto no es así podemos intercambiarlos de tal forma que \(n_2\) sea el primero y \(n_1\) sea el segundo.
  • El valor del exponente de \(n_3\) será igual al del número mayor, \(n_1\), es decir, \(exponente_3 = exponente_1\).
  • Se calcula la diferencia de los exponentes de \(n_1\) y de \(n_2\), es decir, \(DiferenciaExponentes = exponente_1 – exponente_2\).
  • Desplazamos hacia la izquierda el punto flotante de \(n_2\) usando el valor obtenido en \(DiferenciaExponentes\) de tal forma que los exponentes de \(n_1\) y \(n_2\) sean iguales.
  • Calculamos la suma (o resta, que dependerá del signo de los números) de las mantisas. Si el signo de ambos números es el mismo entonces sumamos, si es diferente, entonces restamos.
  • Normalizamos la mantisa resultante, si es necesario, y actualizamos el valor del exponente para tener en cuenta la normalización de la mantisa.

¿En ejemplo a ver si vemos esto más claro?

Volvemos al ejemplo inicial, \(0.1 + 0.2\). en este caso tendremos que \(n_1=0.2\) y \(n_2=0.1\). Uso float‘s de 64 bits para el ejemplo.

Número decimalSignoExponenteMantisa
0.2001111111100 (1020)1001100110011001100110011001100110011001100110011010
0.1001111111011 (1019)1001100110011001100110011001100110011001100110011010
  • ¿Los exponentes son iguales? No
  • Calculamos la diferencia de exponentes: \(1020 – 1019 = 1\). Gracias a eso vemos que tenemos que mover el punto decimal de la mantisa de \(n_2\) hacia la izquierda una única posición:

El número original con el valor 1. invisible que venimos comentando desde el principio

$$ (1.)1001100110011001100110011001100110011001100110011010 (2^{1019}) $$

Añadimos tantos ceros como saltos del punto flotante necesitemos (diferencia de exponentes)

$$ 0(1.)1001100110011001100110011001100110011001100110011010 (2^{1019}) $$

Mantisa que nos queda después de desplazarla hacia la izquierda

$$ 0.11001100110011001100110011001100110011001100110011010 (2^{1020}) $$

Si ahora sumamos las mantisas:

Número decimalMantisa (exponente)
0.21.1001100110011001100110011001100110011001100110011010 (2^1020)
0.10.11001100110011001100110011001100110011001100110011010 (2^1020)
Resultado10.0110011001100110011001100110011001100110011001100111 (2^1020)
1.00110011001100110011001100110011001100110011001100111 (2^1021)

Si ahora nos quedamos con los 52 bits que podemos tener (después de mover el punto flotante nos sobra un bit que añadimos al número final):

Mantisa (exponente)
1.00110011001100110011001100110011001100110011001100111 (2^1021)
——————————————————————–
1 (2^1021)
1.0011001100110011001100110011001100110011001100110011 (2^1021)
—————————————————————
1.0011001100110011001100110011001100110011001100110100 (2^1021)

Por último, el anterior número lo vamos a convertir en decimal. Usando un bucle lo podríamos hacer así:

Y el resultado será:

Que se pasa de frenada si lo comparamos con usar directamente el valor 0.3 en una variable. Por ello:

Dará False como resultado.

Espero que se haya entendido algo. La verdad es que resulta complicado explicar esto sin usar vídeo o una pizarra y sin tiempo de preparar animaciones de algunas cosas. Por favor, si algo no ha quedado claro, seguro que es así, puedes usar los comentarios para preguntar.

Referencias:

Deja una respuesta

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

six + = 7