24 mayo, 2009

CSI XVI: los números de los computadores (II)

Si recuerdan el ya lejano último post acerca de la informática, recordarán que había motivos para estar mosqueado. Muy mosqueado, de hecho, porque vimos que los bancos nos podían chulear con facilidad la parte fraccionaria de los ahorros. Pero eso es porque todavía no les he enseñado cómo representar números reales en los computadores. Y tranquilos, que eso es precisamente lo que voy a comentar hoy.

Ustedes los humanos emplean un punto (o una coma) o notación científica para representar un número con decimales. Pero la cosa es un poco más compleja si no se pueden valer de esos artificios. Para expresar números reales en un ordenador, la solución más sencilla es convenir que una parte de la ristra de unos y ceros represente la parte entera y el restos de unos y ceros representen la parte decimal. Se dedican n bits para la parte entera y p para la fraccionaria. Por supuesto, n y p son fijos para un computador, para no hacerse un lío. Por eso este sistema se denomina de coma fija.


La coma fija mola, porque no tenemos que aprender nada nuevo (aparte de lo que aprendieron en el anterior post). Con 8 bits podemos dedicar 5 a la parte entera y otros 3 a la parte decimal, por ejemplo. De manera que si usamos Complemento a 2 para representar la primera (para la decimal no empleamos ninguna codificación especial) entonces podemos representar un rango de números que va desde -16.0 hasta +15.875. La precisión la marca la cantidad de bits en la parte fraccionaria, claro, que en este caso es de 0.125 (1/(2^3)).

La coma fija no mola, porque operaciones entre números dentro del rango de representación exceden con facilidad el propio rango. Un ejemplo decimal: 0.345 * 0,001 = 0.000345. Si gastamos 3 dígitos para la parte entera y tres para la parte decimal, es evidente que el resultado de la multiplicación con este sistema sería 0. Y nos volvemos a quedar sin céntimos en los bancos.

Para hacer esto más flexible se inventó la coma flotante. La coma flotante es un lío, pero que es muy útil. Un número real se representa como:

R = M * BE donde:
M es número fraccionario, donde se utiliza un bit para representar el signo (como en Signo y magnitud).
B es la base. 2, en casi todos los casos.
E es el exponente, que se representa en exceso Z (esto no lo he explicado).


Si lo pensamos un poco, esto no es más que una aproximación a la notación científica. Por ejemplo, -0.000456 = -456 * 10-6, donde el signo es negativo, la mantisa 456, la base 10 y el exponente -6. La complejidad de la coma flotante es ajustar cada campo a un número limitado de bits. Y para eso vamos a aprovechar hasta el máximo.

El formato IEEE754 de simple precisión (el más conocido de representación de reales) emplea un total de 32 bits: 1 para el sigo, 8 para el exponente y 24 para la mantisa.

¿Cómo?. ¿Que no le salen las cuentas?. Claro, amigo, lo que usted no sabe es que los ingenieros aprovechamos todo el espacio que podemos. Y le ganamos un bit a la mantisa de forma sencilla. Porque, si usted quiere utilizar el formato IEEE754 es porque tiene algo que representar, aunque sea muy pequeño. Un ejemplo:

0,0000101 (binario). ¿Por qué tengo que almacenar en la mantisa los cuatro ceros? Al fin y al cabo, 0,0000101 = 1,01 * 2-5. Sólo tengo que ajustar el exponente (restándole 5). Lo bueno es que esto lo puedo hacer para todos los números, de forma que, sea cual sea el número a representar, el primer bit de la mantisa será 1. Y como el primer bit de la mantisa es siempre 1, me puedo ahorrar el representarlo (¡siempre que luego me acuerde de él a la hora de interpretar el número!). De este modo tenemos 24 bits para representar la mantisa, aunque sólo empleemos 23.

A la operación de dejar un único bit significativo (1) a la izquierda de la coma se le llama normalizar la mantisa.

Hemos dicho que tras normalizar la mantisa conviene ajustar el exponente. Pero no hemos hablado de cómo se representa este mismo, ni qué significa exceso Z. Es una cosa tan tonta que da risa. En exceso Z se escoge cualquier número (Z) para representar el cero. Cualquier número entero A se expresa como el natural A+Z. Por ejemplo, en exceso 127, el -7 se expresa como 120 (-7 + 127). El 3 se expresa como 130 (3 + 127).

Precisamente 127 es el Z escogido para representar el exponente en el formato IEEE754. Por tanto, ya disponemos de todos los elementos para ponernos a calcular números reales. Buenos, nos falta dar el orden de los campos: Signo - Exponente - Mantisa. Ya está.

Representar -209.5625 en coma flotante IEEE754 (simple precisión)
S = 1 (signo negativo)
Parte entera: 209 = 11010001
Parte fraccionaria: .5625 = 0.1001 (2-1 + 2-4)
Mantisa: (parte entera.parte fraccionaria) 11010001.1001 = 1.10100011001 * 2+7
Exponente Z = 127 + 7 = 134, que en binario es 10000110

                        S E        M
De lo cual: -209.5625 = 1 10000110 10100011001000000000000


Y ya hemos aprendido a representar números reales. Por supuesto, al ser un rango de representación finito, con este formato también tenemos cierto límite en la precisión (determinado por los 24 bits de mantisa). Para los más avezados dejo su cálculo.

Yo quiero incidir en otra cosa, que no sé si habrán notado. A pesar de que hemos detallado la representación de números en distintos formatos, al final, en su computador, todos los formatos no son sino ristras de unos y ceros, de modo que la misma longaniza puede ser un número entero negativo, real, o positivo. O no ser un número. Por eso a la hora de escribir un programa es fundamental dar a conocer al computador cómo debe interpretar un valor. Esto se consigue con los tipos de datos.

Cuando queremos gastar un valor, empleamos variables para almacenarlas. Y en gran parte de lenguajes es necesario declarar el tipo de dato de una variable antes de gastarla, para que el computador sepa cómo debe operar con ella.

En C, el lenguaje más extendido, si declaramos una variable como int (entero) lo más posible es que le digamos al computador que esa variable debe representarse en Complemento a 21. Si declaramos un float es muy posible que para representar esa variable se gasten 32 bits (simple precisión) en IEEE754. Y si declaramos un unsigned char, entonces estamos gastando 8 bits en una representación binaria sin signo (positivo).

Y ahora el chiste:

Si los androides algún día sueñan con ovejas eléctricas no olvides declarar el contador de ovejas como long int.



¿Que ha pasado?. Que el contador de ovejas estaba declarado como int (16 bits). Y el rango de representación es (Ca2) -32768 (1000000 00000000) - 32767 (01111111 1111111). Al sumar uno a 32767 el número de ovejas se convierte en negativo. De haber declarado el contador como long int, posiblemente habría tenido 32 bits para representar la cuenta. Eso significa que un androide insomne habría contado hasta 2147483647 (231-1) antes de que sucediera lo mismo. Y si declarara la variable como unsigned long (sin signo), podría contar hasta 4294967295. La siguiente cuenta sería 0, ya que el computador jamás interpretaría este número como negativo.

Ojalá me disculpen. Tan sólo quería compartir humor friqui con ustedes.

1 El número de bits con los que se representa y el formato de representación dependen de la arquitectura del procesador. Por eso es un error asumir que un int tiene siempre, por ejemplo, 32 bits.

Apuntes clase. Pero en la wikipedia también viene bien explicado
Pinar me pasó el link a la tira: más humor en su web.

Posts relacionados

2 cosillas:

Hugo dijo...

Tus artículos sobre informática me fascinan, y a la vez no hacen más que recordarme cuanto tiempo hace que abandoné el camino de las ciencias verdaderas por una hermana bastarda de estas xD

Delirium dijo...

Hombre, pues muchas gracias. Pero has de tener en cuenta que hago trampa: sólo cuento las cosas que me parecen interesantes de una carrera de 5 años. El resto se lo ahorro a los sufridos lectores.

Y te aseguro que el resto, por aburrido o chorra, hace que mucha gente que empieza a estudiar Informática se desanime enseguida.

Un saludo y gracias por el comentario. De veras creía que nadie leía estos posts.