Representación numérica en un ordenador

De MateWiki
Saltar a: navegación, buscar

Los ordenadores suelen usar un sistema binario de representación de números, debido a que están fabricados con componentes electrónicos que son capaces de representar solo dos estados diferentes. Además, este sistema de representación es discreto, lo que supone un problema para representar números reales en un ordenador. Si estamos usando un método numérico para resolver un problema de ingeniería o científico, es importante entender cómo se representa la información en el ordenador para controlar el error cometido en el cálculo.

1 Números naturales

Los dispositivos electrónicos que forman parte del ordenador están fabricados usando transistores. Un transistor es un elemento electrónico biestable, es decir, es capaz de representar dos estados. Mediante impulsos eléctricos es posible leer el estado de un transistor, y cambiarlo. Debido a esta característica, en la actualidad todos los ordenadores funcionan con un sistema de representación numérica binario. En este sistema, solo existen dos cifras diferentes: 0 y 1. Por ejemplo, el sistema decimal, usado habitualmente por las personas, tiene diez cifras diferentes: 0, 1, 2, 3, 4, 5, 6, 7, 8 y 9.

Toda la información que se guarda y procesa en el ordenador se transforma al sistema de representación numérica binario. El caso más sencillo de entender es el de los números naturales (aunque en los ejemplos incluiremos el cero también). Tradicionalmente, representamos los números naturales usando el sistema decimal de representación. Pero se pueden representar en cualquier otro sistema, de manera sencilla. En la siguiente tabla se muestran algunos ejemplos:

Decimal Binario
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000

El sistema parece bastante ineficiente. Por ejemplo, para representar el número 8 se necesitan cuatro cifras (1000). Sin embargo, cada una de las cifras del número en binario se representa en el ordenador por un transistor. En el sistema de representación de información, cada una de las cifras recibe el nombre de bit. Cuando un número contiene 8 bits, recibe el nombre de byte[1].

Los transistores actuales son microscópicos, por lo que un ordenador puede contener cientos de millones de transistores, lo que posibilita la representación de números muy grandes. Por ejemplo, los procesadores Intel Core i5 pueden contener hasta 700 millones de transistores[2]. En estos procesadores, teóricamente sería posible entonces representar números naturales hasta el [math]2^{700\cdot 10^6}[/math] en sistema decimal. Este número tendría 210 millones de cifras[3] en sistema decimal. En la realidad, la representación de números naturales no puede alcanzar números con esa enormidad de cifras. En primer lugar, muchas operaciones requieren al menos dos números, lo que reduciría la cantidad de cifras para cada número a la mitad. Además, algunos bits se dedican a controlar el formato de los números, su posición en la memoria del ordenador, a representar la operación que se realizará con esos números, etc. En la práctica, las arquitecturas modernas funcionan con números de 64 bits.

Si usamos 64 bits para representar un número natural en sistema decimal, el número más grande que podremos representar es [math]2^{64}-1\approx 1.84\cdot 10^{19}[/math] (restamos 1 para contar el cero también, aunque no sea un número natural), que es un número con 20 cifras en sistema decimal. Podemos comprobarlo con Octave UPM. Si estamos usando un ordenador con arquitectura de 64 bits, podemos escribir el siguiente comando para comprobar el número natural más grande que podemos representar:

>> intmax('uint64')
ans = 18446744073709551615

El tipo uint64 se usa para representar enteros sin signo de 64 bits. Un entero sin signo incluye los numeros naturales y el número cero. Podemos comprobar que es un número de 64 bits, calculando su logaritmo en base 2:

>> log(intmax('uint64'))/log(2)
ans =  64

Existen también otros tipos de enteros más cortos:

Tipo intmax Bits
uint8 255 8
uint16 65535 16
uint32 4294967295 32

2 Números enteros

Para representar los números enteros tenemos que tener en cuenta que es necesario también incluir el signo. Como solo tenemos dos signos posibles (positivo o negativo), es posible representar el signo con un bit. Como en Octave UPM la longitud máxima de un número es 64 bits, esto deja solo 63 bits disponibles para la representación numérica. Podemos comprobar cuáles son los números más grande y pequeño que podemos representar y su longitud en bits:

>> intmax('int64')
ans = 9223372036854775807
>> intmin('int64')
ans = -9223372036854775808
>> log(intmax('int64'))/log(2)
ans =  63

Podemos comprobar que el valor intmax('int64') es más pequeño que intmax('uint64').

2.1 Visualización de los tipos enteros en Octave UPM

En Octave UPM podemos visualizar fácilmente la representación binaria de cualquier número usando el comando format. El comando format acepta la opción bit para mostrar la representación en binario de los números, que es como los guarda internamente Octave UPM. Se puede volver al comportamiento habitual (mostrar cuatro decimales) ejecutando format sin ningún argumento. La opción bit de format no está disponible en MATLAB, solo se puede usar en Octave UPM o GNU Octave.

Para los ejemplos que vamos a mostrar aquí, vamos a forzar a que todos los números usen los tipos de datos enteros, ya que por defecto Octave UPM usa el tipo de datos double. Veamos el caso de los enteros sin signo, que usábamos para representar a los números naturales más el cero.

>> format bit
>> uint64(0)
ans = 0000000000000000000000000000000000000000000000000000000000000000
>> uint32(0)
ans = 00000000000000000000000000000000
>> uint16(0)
ans = 0000000000000000
>> uint8(0)
ans = 00000000

Podemos ver como un entero sin signo de 64 bits usa 64 cifras binarias para representar el número cero. Del mismo modo, si usamos tipos con menos bits, la cantidad de ceros es menor. El número más grande que podemos representar es el que pone todas las cifras binarias 1. Veamos algunos ejemplos:

>> uint64(1)
ans = 0000000000000000000000000000000000000000000000000000000000000001
>> uint64(2)
ans = 0000000000000000000000000000000000000000000000000000000000000010
>> uint64(3)
ans = 0000000000000000000000000000000000000000000000000000000000000011
>> uint64(4)
ans = 0000000000000000000000000000000000000000000000000000000000000100
>> uint64(5)
ans = 0000000000000000000000000000000000000000000000000000000000000101
>> uint64(6)
ans = 0000000000000000000000000000000000000000000000000000000000000110
>> uint64(7)
ans = 0000000000000000000000000000000000000000000000000000000000000111
>> uint64(123)
ans = 0000000000000000000000000000000000000000000000000000000001111011
>> uint64(4513)
ans = 0000000000000000000000000000000000000000000000000001000110100001

Los primeros números muestran los mismos resultados que la tabla mostrada en una sección anterior. Como podemos deducir, el número más grande que se puede representar es aquel que ponga todos los bits a 1:

>> intmax('uint64')
ans = 1111111111111111111111111111111111111111111111111111111111111111

En cambio, si ejecutamos lo siguiente:

>> intmax('int64')
ans = 0111111111111111111111111111111111111111111111111111111111111111

Vemos que el primer bit no se usa para representar el número. Eso es porque en el tipo int64 el primer bit se usa para representar el signo del número y solo se aprovechan 63 bits para la representación del número. En cambio, como el tipo uint64 no tiene signo, aprovecha todos los bits para la representación del número.

3 Números reales

La representación de los números enteros y naturales es relativamente sencilla. Después de todo, al ser discretos, se representan bien en el sistema binario. El único inconveniente es que existe un límite para los números que podemos representar debido a que el ordenador tiene un número finito de transistores. ¿Pero qué ocurrirá con los números reales? El conjunto de los números reales no es numerable[4], es decir, entre dos números reales dados existe una cantidad infinita de números reales. ¿Cómo podemos representar esta cantidad infinita de números en un ordenador con un número finito de transistores?

La representación de números reales se realiza con los denominados números de punto flotante[5]. A grandes rasgos, un número en punto flotante tiene dos partes: la parte entera y la parte decimal. Más en detalle, la representación del número incluye:

  • La mantisa, que contiene todos los dígitos del número. Puede contener signo.
  • El exponente, que indica la posición del punto decimal, respecto al inicio de la mantisa. También puede contener signo.

Es decir, la representación en punto flotante es similar a la notación científica. Por ejemplo, el número [math]12.345[/math] se representaría por una mantisa formada por los números 12345 y el exponente 1. El número se obtendría multiplicando [math]1.2345 \cdot 10^1[/math]. Otro ejemplo sería el número [math]0.00675[/math], cuya representación constaría de la mantisa 675 y del exponente -3; el número se obtendría como [math]6.75\cdot 10^{-3}[/math].

3.1 Precisión doble y sencilla

En arquitecturas de 64 bits, la mantisa, el signo de la mantisa, el exponente y el signo del exponente, pueden ocupar como máximo 64 bits. Esto hace que los números más grande y pequeño que podemos representar en punto flotante sean más pequeños que en el caso de los enteros y los naturales. En el caso de Octave UPM, existen dos tipos diferentes para representar números en punto flotante:

  • single, que ocupa 4 bytes (32 bits), también denominado float o de precisión sencilla
  • double, que ocupa 8 bytes (64 bits), también denominado de precisión doble

Si no indicamos nada, todos los cálculos se realizan con el tipo double (incluso cuando usamos números sin parte decimal).

Los límites que podemos representar con estos números son los siguientes:

Tipo realmin realmax
single [math]1.17549435082229\cdot 10^{-38}[/math] [math]3.40282346638529\cdot 10^{38}[/math]
double [math]2.22507385850720\cdot 10^{-308}[/math] [math]1.79769313486232\cdot 10^{308}[/math]

3.2 Error debido a la precisión

En ambos casos, como todo el espacio que ocupa el número tiene que repartirse entre mantisa y exponente, existe también un límite en la precisión que podemos representar. Es decir, los números en punto flotante son discretos. Por ejemplo, si tomamos un número con infinitos decimales, inevitablemente se perderán decimales al representarlo en punto flotante. Este error se conoce como error de redondeo, ya que el número decimal se redondea ignorando los decimales de menor peso.

Veamos un ejemplo. El número [math]\sqrt{2}[/math] tiene infinitos decimales. Por tanto, al representarlo en punto flotante, se perderán algunos decimales. Sin embargo, la pérdida de números decimales no es aparente, ya que Octave UPM muestra cuatro decimales por defecto, y como máximo 14 decimales al imprimir en pantalla:

>> format long     % Para mostrar 14 decimales 
>> 1+1
ans = 2
>> sqrt(2)
ans =  1.41421356237310
>> ans^2
ans =  2.00000000000000

Sin embargo, aunque el primer y último resultados parecen iguales, en el segundo caso Octave UPM ha mostrado algunos ceros que normalmente no pone. ¿Por qué aparecen los ceros? En este caso, al calcular [math](\sqrt{2})^2[/math] no ha obtenido un resultado exacto, ya que ha tenido que redondear el número [math]\sqrt{2}[/math]. Al no obtener un resultado exacto, hay algunos decimales de más, pero con un peso tan pequeño que no aparecen en las primeras 14 posiciones.

Podemos averiguar cuál ha sido el error que ha cometido en el cálculo:

>> 2 - sqrt(2)^2
ans = -4.44089209850063e-16

En este caso, la operación debería dar cero como resultado, pero vemos que hay diferencia a partir del decimal número 16. Puede parecer un error pequeño, pero si no se tiene en cuenta, y estamos realizando un cálculo de ingeniería, se puede producir una propagación y acumulación de errores que dé lugar a un resultado final con un error inaceptable.

En Octave UPM, podemos averiguar cuál es la precisión más pequeña que podemos lograr usando el comando eps. Veamos la precisión de los tipos en punto flotante:

>> eps('single')
ans =  1.19209289550781e-07
>> eps('double')
ans =  2.22044604925031e-16

4 Referencias

  1. Este sistema tiene su origen en los primeros microprocesadores, que eran capaces de manejar números de 8 bits. En la actualidad, las arquitecturas de los ordenadores personales trabajan con números de 64 bits. Véase Byte (Wikipedia ES).
  2. List of Intel Core i5 microprocessors (Wikipedia EN)
  3. Para calcular la cantidad de cifras en decimal m correspondiente a n bits calculamos $$m = \log_{10} 2^n = n \log_{10} 2$$. En este caso, hemos calculado $$700\cdot 10^6 \log_{10} 2 = 2.10\cdot 10^8$$
  4. Conjunto no numerable (Wikipedia ES)
  5. Números de punto flotante. La guía del punto flotante