1. Números enteros en Julia

Material desarrollado por Arturo Erdely para aprender programación en Julia. Más información en: Programación en Julia

BigInt bitstring Bool convert Int8 Int16 Int32 Int64 Int128 length println sizeof typemax typemin typeof UInt8 UInt16 UInt32 UInt64 UInt128 # \t _

Es posible realizar operaciones directamente en la línea de comandos:

4 - 99
4.5s
-95

Asignar valores a variables con nombres que distinguen minúsculas de mayúsculas. Aquello que se escriba después del símbolo # se considera comentario y por tanto Julia no intenta ejecutarlo como código.

valorBestial = 666 # esto es un comentario
0.2s
666

Mediante println podemos mostrar el valor de una variable o el resultado de alguna operación:

println(valorBestial + 333)
0.8s

También es posible utilizar caracteres especiales mediante comandos tipo LaTeX\LaTeX. Por ejemplo, mediante \theta seguido de la tecla de tabulador (TAB) se obtiene el símbolo δ\delta mismo que puede utilizarse como nombre de objeto:

δ = 111
println(δ + 999)
0.6s

Los números tipo Integer (enteros) tienen los siguientes subtipos:

  • Signed (con signo) que pueden ser a 8, 16, 32, 64 o 128 bits: Int8, Int16, Int32, Int64, Int128

  • Unsigned (sin signo): UInt8, UInt16, UInt32, UInt64, UInt128

  • Bool (booleano o lógico)

  • BigInt (entero de precisión arbitraria)

El tipo por defecto (default) será el de los bits que corresponda al equipo de cómputo que se esté utilizando, mismo que puede saberse mediante:

println(Sys.WORD_SIZE) # computadora actual ¿a cuántos bits?
0.6s

1.1 Tipos Signed y Unsigned

Si asignamos un valor entero sin especificar el tipo de entero, será el tipo por default del equipo de cómputo que se esté utilizando, mismo que puede averiguarse mediante typeof:

x = 5
println(typeof(x))
0.8s

Si queremos especificar un tipo de entero distinto al tipo por defecto del equipo que se está utilizando, se hace mediante convert:

y = convert(Int8, x)
println(typeof(y))
0.4s

O simplemente:

z = Int8(5)
println(z)
println(typeof(z))
0.8s

Para comprobar la diferencia sizeof proporciona el tamaño en bytes de un objeto:

sizeof(x), sizeof(y)
0.6s
(8, 1)

Ahora comparemos sus representaciones como cadenas de bits con la función bitstring:

bitstring(x) # el resultado aparece entre comillas por tratarse de una cadena de caracteres
0.7s
"0000000000000000000000000000000000000000000000000000000000000101"

Recordando que 1 byte = 8 bits entonces 8 bytes = 64 bits. Lo comprobamos con length:

length(bitstring(x))
0.0s
64
bitstring(y)
0.0s
"00000101"
length(bitstring(y))
0.0s
8

Así, con enteros tipo Int8 su representación como cadena de 8 bits es de la forma b7b6b5b4b3b2b1b0b_7b_6b_5b_4b_3b_2b_1b_0 donde cada bk{0,1}b_k\in\{0, 1\} y corresponde al entero k=07bk×2k.\sum_{k=0}^7 b_k\times 2^k. Así, por ejemplo, el entero 55 como tipo Int8 se representa mediante 00000101 esto es:

bitstring(Int8(5))
0.0s
"00000101"

Mediante cadenas de 8 bits que solo pueden tomar los valores 0 y 1 solo es posible representar un total de 28=2562^8 = 256 valores distintos. Mediante las funciones typemin y typemax podemos averiguar el entero mínimo y máximo representable mediante el tipo Int8:

typemin(Int8), typemax(Int8)
0.9s
(-128, 127)

Sucesivamente, comenzando en -128 y terminando en 127 tenemos un total de 256 números enteros distintos. El máximo entero tipo Int8 se representa mediante 01111111:

bitstring(Int8(127))
0.0s
"01111111"

y el mínimo entero positivo tipo Int8 se representa mediante la representación binaria que sigue a 01111111 que es 10000000:

bitstring(Int8(-128))
0.0s
"10000000"

Nótese que el cero es simplemente 00000000:

bitstring(Int8(0))
0.0s
"00000000"

Esto es, las cadenas de 8 bits iniciando en 00000000 y hasta 01111111 corresponden a los enteros 0, 1, 2, ..., 127. La cadena de 8 bits que sigue a 01111111 es 10000000 y se asocia al entero mínimo -128 como podemos comprobar:

bitstring(Int8(-128))
0.0s
"10000000"

Luego -127, -126, y así sucesivamente hasta llegar a -1:

bitstring(Int8(-127)), bitstring(Int8(-126)), bitstring(Int8(-1))
0.6s
("10000001", "10000010", "11111111")

Esto es, con cadenas de 8 bits, los enteros tipo Int8 pueden representar un total de 256 números enteros distintos, de los cuales la mitad (128) son los enteros no negativos del 0 al 127, y luego la otra mitad (128) los enteros negativos de -128 a -1. Así que mediante representaciones binarias sucesivas de 8 bits se codifican, en el siguiente orden, los enteros: 0, 1, ..., 126, 127, -128, -127, -126, ..., -2, -1. ¿Por qué en este orden? Hay una razón para ello que se ilustra con un sencillo ejemplo:

cincoPositivo = bitstring(Int8(5))
cincoNegativo = bitstring(Int8(-5))
(cincoPositivo, cincoNegativo)
5.3s
("00000101", "11111011")

Si hacemos la suma directa de estas dos representaciones binarias:

00000101

+

11111011

-------------

100000000

obtenemos una representación binaria de 9 bits, pero como solo tenemos (en este caso) espacio para 8 bits, el adicional se ignora (y por tanto se trunca) y queda el resultado como 00000000 (cero) que es justamente lo que corresponde a 5 + (-5) = 0.

El procedimiento anterior se conoce como codificación de enteros con signo en complemento a 2 que de forma general consiste en lo siguiente: en palabras de 𝑛 bits se reserva la representación binaria a enteros no negativos z{0,1,,2n11}z\in\{0,1,\ldots,2^{n-1}-1\} para después representar a enteros negativos z{(2n1),(2n1)+1,,2,1}-z\in\{-(2^{n-1}),-(2^{n-1})+1,\ldots,-2,-1\} como el entero no negativo 2nz2^{n}-z y así garantizar que z+(z)=z+2nz=2nz + (-z) = z + 2^n - z = 2^n, que al truncarle el último bit queda como cero. Para más detalle sobre esto se recomienda ampliamente el libro Numerical Computing with IEEE Floating Point Arithmetic de Michael L. Overton (SIAM, 2001), y que cuenta con traducción al idioma español como Cómputo Numérico con Aritmética de Punto Flotante IEEE (Sociedad Matemática Mexicana, 2002).

Si renunciamos al signo, mediante 8 bits podemos representar los enteros del 0 al 255 mediante enteros tipo UInt8:

typemin(UInt8), typemax(UInt8) # muestra representación hexadecimal
0.6s
(0x00, 0xff)
println(typemin(UInt8)) # representación decimal
println(typemax(UInt8)) # representación decimal 
0.8s

Es importante tener presente lo que sucederá si realizamos operaciones con números enteros cuyo resultado exceda el rango de representación del tipo de entero en cuestión. Por ejemplo, si al entero 255 tipo UInt8 le sumamos 1, esto convertirá a la cadena de bits 11111111 en 00000000, esto es 255+1=0255 + 1 = 0 y que nadie se desgarre las vestiduras:

a = convert(UInt8, 255)
b = UInt8(1)
c = a + b
println(c)
println(typeof(c))
0.6s

Volviendo al ejemplo donde al entero con signo 5-5 tiene como representación en 8 bits a 11111011,11111011, esto corresponde al entero sin signo 285=2512^8 - 5 = 251 y por lo tanto 251251 como entero sin signo de 8 bits tiene la misma representación binaria que el entero con signo 5-5 en 8 bits:

bitstring(UInt8(251)), bitstring(Int8(-5))
4.6s
("11111011", "11111011")

De hecho, lo anterior podía verificarse por medio de la función reinterpret:

println(reinterpret(UInt8, Int8(-5)))
println(reinterpret(Int8, UInt8(251)))
4.5s

Así que en operaciones con enteros, de cualquier tipo, es importante estar consciente de los valores mínimo y máximo que puede manejar, para que no haya sorpresas con los resultados:

println("Int8 \t", 8*sizeof(Int8), " bits \t min = ", typemin(Int8), "\t max = ", typemax(Int8))
println("UInt8 \t", 8*sizeof(UInt8), " bits \t min = ", typemin(UInt8), "\t max = ", typemax(UInt8))
println("Int16 \t", 8*sizeof(Int16), " bits\t min = ", typemin(Int16), "\t max = ", typemax(Int16))
println("UInt16 \t", 8*sizeof(UInt16), " bits\t min = ", typemin(UInt16), "\t max = ", typemax(UInt16))
println("Int32 \t", 8*sizeof(Int32), " bits\t min = ", typemin(Int32), "\t max = ", typemax(Int32))
println("UInt32 \t", 8*sizeof(UInt32), " bits\t min = ", typemin(UInt32), "\t max = ", typemax(UInt32))
println("Int64 \t", 8*sizeof(Int64), " bits\t min = ", typemin(Int64), "\t max = ", typemax(Int64))
println("UInt64 \t", 8*sizeof(UInt64), " bits\t min = ", typemin(UInt64), "\t max = ", typemax(UInt64))
println("Int128 \t", 8*sizeof(Int128), " bits min = ", typemin(Int128), "\t max = ", typemax(Int128))
println("UInt128 ", 8*sizeof(UInt128), "bits\t min = ", typemin(UInt128), "\t max = ", typemax(UInt128))
1.1s

Por ejemplo, lo siguiente genera un error por intentar convertir un entero fuera del rango admisible para el tipo que se pide:

Int8(128)
1.7s

1.2 Tipo Bool

Aunque solo admite dos valores distintos false y true su representación es en 8 bits:

println("Bool \t", 8*sizeof(Bool), " bits \t min = ", typemin(Bool), "\t max = ", typemax(Bool))
0.8s
d = false
e = true
println(typeof(d))
println(bitstring(d))
println(bitstring(e))
0.8s

1.3 Tipo BigInt

Números enteros de precisión arbitraria. En este caso no existe una representación binaria de tamaño específico, de hecho la instrucción bitstring(BigInt(2)) arroja un error. Con BigInt se puede tener tanta precisión en un número como lo permita el tamaño de la memoria de una computadora. Pero tiene un costo importante, además del consumo de memoria: menor velocidad en las operaciones. Si solo se realizarán unas pocas operaciones no se sentirá el impacto, pero ante un gran número de operaciones con números tipo BigInt sí que comenzará a sentirse la diferencia respecto al uso de números con representación binaria fija a cierto número de bits. Esto ocurre porque las operaciones con representaciones binarias de número fijo de bits las realiza el hardware de la computadora, en contraste con las operaciones de números de precisión arbitraria que se realizan desde el software, y es eso último lo que provoca baja velocidad ante un número elevado de operaciones.

enteromaxU128 = typemax(UInt128) # máximo entero sin signo de 128 bits
println(enteromaxU128)
0.6s
println(enteromaxU128 + 3) # por exceder el valor máximo para un UInt128
0.5s
bb = BigInt(enteromaxU128)
bb3 = bb + 3
println(typeof(bb3), "\t", 8*sizeof(bb3), " bits")
println(bb3)
0.9s

Así, por ejemplo, una forma incorrecta de calcular algo tan grande como 35003^{500} sería:

3^500
0.0s
-3526474796859007727

En este caso utilizar tipo BigInt:

BigInt(3)^500
0.7s
36360291795869936842385267079543319118023385026001623040346035832580600191583895484198508262979388783308179702534403855752855931517013066142992430916562025780021771247847643450125342836565813209972590371590152578728008385990139795377610001

Julia permite utilizar un separador de dígitos _ para una más cómoda lectura. Por ejemplo:

cifra = 100_000_000
0.0s
100000000

Material desarrollado por Arturo Erdely para aprender programación en Julia. Más información en: Programación en Julia

Runtimes (1)