Armadilhas Numéricas – Reais- Parte I

Este é o terceiro artigo de uma série de artigos sobre a representação de números nas plataformas de programação modernas como C# e  Java, os problemas práticos derivados dessas representação, erros comuns dos programadores e formas OO de resolver e lidar com as limitações.

Vimos no artigo anterior como é feita a representação de números inteiros em forma binária, que problemas isso levanta e como são resolvidos com a ajuda da Orientação a Objetos. Veremos neste artigo como é feita a representação de números reais e em particular como a representação é feita conforme a norma IEEE754 que é usada nas plataformas modernas.

Números Reais

Os números reais formam o conjunto |R. O conjunto dos reais é um magma em todas as quatro operações aritméticas e, portanto é mais útil em cálculos genéricos como em engenharia, por exemplo. Contudo linguagens e máquinas antigas estavam limitadas a representar números inteiros como um conjuntos de bits. Foi então necessário estabelecer um padrão para utilizar os bits para representar números reais.

À diferença dos inteiros, os reais se dividem em dois subgrupos: os racionais e os irracionais. Os racionais são todos os números que podem ser representados como frações de dois inteiros.  Os irracionais são os que não podem ser representados assim. É um fato conhecido da matemática que existem mais números irracionais que racionais, mas felizmente não temos que lidar com eles a maior parte do tempo pois eles só aparecem quando lidamos com geometria ou limites.  Contudo há muitos números irracionais importantes como π, e , raiz quadrada de 2 ou  o número de ouro (também conhecido como número áureo ou proporção áurea). Como curiosidade, a definição do número de ouro (ϕ) é: “o número cujo quadrado é a soma dele com 1”, ou seja o número que resolve  a equação ϕ + 1 = ϕ2.

Um número irracional é em si mesmo irrepresentável por uma decomposição em fatores (se fosse representável assim seria um número racional), então números irracionais não podem ser presentados como um conjunto de bits onde cada bit representa a presença de certo fator. Números irracionais são irrepresentáveis desta forma. Esta propriedade é em geral válida para qualquer número real e apenas alguns números reais específicos podem ser representados desta forma. Já que a representação binária é baseada na representação em fatores, isto representa um problema para representar números reais como fatores de base 2.

A representação binária, tal como a decimal não é exata. Por exemplo, 1/3 é facilmente representável como um número real racional, mas a sua representação decimal é 0.(3) , onde o parêntesis em torno do 3 significa que esse dígito se repete infinitamente. Isto acontece porque estamos usando a representação decimal, que equivale a usar potências negativas de 10. Logo

1/3 = 0 x 100 + 3 x 10-1 + 3 x 10-2 + 3 x 10-3 + 3 x 10-4 + 3 x 10-5 + 3 x 10-6 + …

Para representar o mesmo número em binário, usamos a mesma lógica

1/3 = 0 x 20 + 0 x 2-1 + 1 x 2-2 + 0 x 2-3 + 1 x 2-4 + 0 x 2-5 + 1 x 2-6 + …

Seja em representação binária ou em decimal o problema é o mesmo: são necessários infinitos fatores. Contudo a representação binária tem ainda outro problema, mesmo valores que em decimal são facilmente representáveis como 0.1 geram fatores infinitos em binário. Isto é especialmente ruim,pois valores monetários são normalmente representados com um conjunto fracionário de centavos o que tornar estes valores irrepresentáveis em binários. Algumas moedas, como o Yen, não usam centavos exatamente para não ter o problema de trabalhar com números decimais, usam apenas inteiros. O dinheiro segue o mesmo conjunto de princípios operacionais que o conjunto de operações sobre  números inteiros e, portanto, o mapeamento de moeda para inteiros é muito mais, computacionalmente, exato que o uso de decimais. Mais sobre isso depois.

Esta representação binária em fatores rapidamente se mostra simplista para representar números reais, apresentado várias limitações. Estas limitações foram estudadas e resultaram no conceito de vírgula flutuante (também chamado de “ponto flutuante”) padronizado pela norma  IEEE 754. As máquinas e linguagens modernas se regem por este padrão para trabalhar com números reais em representação binária. É que, embora a representação não seja exata, é suficientemente exata para alguns tipos de cálculo e o uso de uma representação binária torna o cálculo muito rápida e possível de realizar em hardware. Então, o trade-off feito na época foi sacrificar a exatidão pela rapidez do cálculo.

O padrão IEE754 define formas de interpretar os bits e como as operações têm que ser feitas para extrair o máximo de informação e representar o máximo de valores. Existem diferentes sabores de números conforme o número de bits usados. Os mais conhecidos são Float com 32 bits ( também chamado de Single) e Double com 64 bits. Em C# o tipo System.Double da plataforma .Net é uma estrutura (struct) que representa um número real conforme a norma IEEE754 com 64 bits. Também existe o tipo System.Float que é análogo para 32 bits.

A Representação IEEE754 é baseada no mesmo modelo que a representação em Notação Científica. A notação científica é uma forma de representar um valor usando a potência de uma certa base  B e um conjunto limitado de dígitos p.  A notação científica , porque tem uma limitação no número de dígitos que se podem usar, leva ao conceito de Dígito Significativo. Ou seja, nem todos os dígitos representam informação válida e devem ser descartados. Por exemplo, para representar 0.1 podemos escrever, para base 10 (B= 10) com 3 dígitos significativos (p=3):

0.1  = 1.00 x 10-1

Então para representar um número real qualquer r, precisamos saber a base B, o valor do expoente dessa base (-1 no caso) e o valor do significante (1.00 no caso). Como a base é pré-definida, então apenas precisamos preservar a informação sobre dois valores, o expoente e o significante (também chamado mantissa). Contudo, temos que ser capazes de preservar estes dois valores em apenas uma sequência de bits. Como vimos antes, a representação binária só é exata para representar inteiros, então precisamos preservar dois inteiros em uma única sequência de bits. Para isso a norma IEEE745 estabelece  dois conjuntos de bits. O tamanho destes conjuntos de bits depende do tamanho total de bits possíveis. Para 64 bits , a mantissa usa apenas 53 bits. Como é possível representar mais números com menos bits ? Porque estamos usando a multiplicação por uma potência o que nos permite ajustar a representação ao “tamanho” do valor numérico que queremos representar. A diferença entre representar um milionésimo de milionésimo (10-6) ou um milhão (106) é apenas alterar o sinal do expoente.

À semelhança do que vimos para os inteiros, a representação binárias dos reais também trunca o conjunto |R sendo apenas possível representar alguns dos valores. Primeiro, porque remove qualquer opção de representar números irracionais. Segundo porque limita quais elementos de |R podem ser representados com exatidão.

O conjunto |R é denso, o que significa que entre dois números reais quaisquer, é sempre possível, encontrar outro número real. Não há hiatos. Há um continuo de números. A representação pela norma IEEE754 força a existência de hiatos, e nem sempre com o mesmo tamanho. O “espaço” entre dois números reais representados binariamente depende do valor absoluto de ambos os números sendo comparados. Portanto, a representação binária de reais não trunca apenas os valores máximos e mínimos, mas também “o espaço” entre os valores. Os hiatos são criados para ser possível abarcar todos os números reais.

Resumo

Vimos no post anterior como a representação binária de números reais não pode ser feita da mesma forma que a dos inteiros e é realizada com base numa versão da notação científica em base 2 padronizada pela norma IEEE754.  Esta norma utiliza sequências com os mesmos bits que uma sequência usada para representar inteiros, mas divide a sequência em duas áreas para representar dois valores inteiros, a mantissa e o expoente. Assim, a norma se utiliza da rapidez do hardware em manipular operações com bits ao mesmo tempo que consegue representar númenos reais. Contudo, não todos. Números irracionais não podem ser representados e apenas a representação decimal é possível.  No próximo artigo veremos que outros problemas a representação IEEE754 para double e float traz e no último artigo veremos como estes problemas são resolvidos com ajuda da Orientação a Objetos.





Desenvolvido por hacklab/ com WordPress