Por que os números de ponto flutuante são imprecisos?

198

Por que alguns números perdem a precisão quando armazenados como números de ponto flutuante?

Por exemplo, o número decimal 9.2pode ser expresso exatamente como uma razão de dois números inteiros decimais ( 92/10), os quais podem ser expressos exatamente em binário ( 0b1011100/0b1010). No entanto, a mesma proporção armazenada como um número de ponto flutuante nunca é exatamente igual a 9.2:

32-bit "single precision" float: 9.19999980926513671875
64-bit "double precision" float: 9.199999999999999289457264239899814128875732421875

Como um número aparentemente simples pode ser "grande demais" para expressar em 64 bits de memória?

mhlester
fonte

Respostas:

241

Na maioria das linguagens de programação, os números de ponto flutuante são representados de maneira semelhante à notação científica : com um expoente e uma mantissa (também chamada de significando). Um número muito simples, digamos 9.2, é realmente essa fração:

5179139571476070 * 2 -49

Onde está o expoente -49e a mantissa 5179139571476070. A razão pela qual é impossível representar alguns números decimais dessa maneira é que o expoente e a mantissa devem ser inteiros. Em outras palavras, todos os carros alegóricos devem ser um número inteiro multiplicado por uma potência inteira de 2 .

9.2pode ser simples 92/10, mas 10 não pode ser expresso como 2 n se n for limitado a valores inteiros.


Vendo os dados

Primeiro, algumas funções para ver os componentes que compõem um 32 e 64 bits float. Passe por cima deles se você se importa apenas com a saída (exemplo em Python):

def float_to_bin_parts(number, bits=64):
    if bits == 32:          # single precision
        int_pack      = 'I'
        float_pack    = 'f'
        exponent_bits = 8
        mantissa_bits = 23
        exponent_bias = 127
    elif bits == 64:        # double precision. all python floats are this
        int_pack      = 'Q'
        float_pack    = 'd'
        exponent_bits = 11
        mantissa_bits = 52
        exponent_bias = 1023
    else:
        raise ValueError, 'bits argument must be 32 or 64'
    bin_iter = iter(bin(struct.unpack(int_pack, struct.pack(float_pack, number))[0])[2:].rjust(bits, '0'))
    return [''.join(islice(bin_iter, x)) for x in (1, exponent_bits, mantissa_bits)]

Há muita complexidade por trás dessa função, e seria bastante tangível de explicar, mas se você estiver interessado, o recurso importante para nossos propósitos é o módulo struct .

O Python's floaté um número de precisão dupla de 64 bits. Em outras linguagens como C, C ++, Java e C #, a precisão dupla tem um tipo separado double, que é frequentemente implementado como 64 bits.

Quando chamamos essa função com o nosso exemplo 9.2, aqui está o que obtemos:

>>> float_to_bin_parts(9.2)
['0', '10000000010', '0010011001100110011001100110011001100110011001100110']

Interpretando os dados

Você verá que eu dividi o valor de retorno em três componentes. Esses componentes são:

  • Placa
  • Expoente
  • Mantissa (também chamada de Significand, ou Fração)

Placa

O sinal é armazenado no primeiro componente como um único bit. É fácil de explicar: 0significa que o flutuador é um número positivo; 1significa que é negativo. Porque 9.2é positivo, nosso valor de sinal é 0.

Expoente

O expoente é armazenado no componente do meio como 11 bits. No nosso caso 0b10000000010,. Em decimal, isso representa o valor 1026. Uma peculiaridade desse componente é que você deve subtrair um número igual a 2 (# de bits) - 1 - 1 para obter o expoente verdadeiro; no nosso caso, isso significa subtrair 0b1111111111(número decimal 1023) para obter o expoente verdadeiro 0b00000000011(número decimal 3).

Mantissa

A mantissa é armazenada no terceiro componente como 52 bits. No entanto, há uma peculiaridade nesse componente também. Para entender essa peculiaridade, considere um número em notação científica, assim:

6.0221413x10 23

A mantissa seria a 6.0221413. Lembre-se de que a mantissa na notação científica sempre começa com um único dígito diferente de zero. O mesmo vale para o binário, exceto que o binário possui apenas dois dígitos: 0e 1. Assim, a mantissa binária sempre começa com 1! Quando um flutuador é armazenado, a 1parte frontal da mantissa binária é omitida para economizar espaço; temos que colocá-lo de volta na frente do nosso terceiro elemento para obter a verdadeira mantissa:

1.0010011001100110011001100110011001100110011001100110

Isso envolve mais do que apenas uma simples adição, porque os bits armazenados em nosso terceiro componente representam, na verdade, a parte fracionária da mantissa, à direita do ponto de raiz .

Ao lidar com números decimais, "movemos o ponto decimal" multiplicando ou dividindo por potências de 10. Em binário, podemos fazer o mesmo multiplicando ou dividindo por potências de 2. Como nosso terceiro elemento possui 52 bits, dividimos por 2 52 para movê-lo 52 lugares para a direita:

0.0010011001100110011001100110011001100110011001100110

Em notação decimal, é o mesmo que dividir 675539944105574por 4503599627370496obter 0.1499999999999999. (Este é um exemplo de uma proporção que pode ser expressa exatamente em binário, mas apenas aproximadamente em decimal; para obter mais detalhes, consulte: 675539944105574/4503599627370496 .)

Agora que transformamos o terceiro componente em um número fracionário, a adição 1fornece a verdadeira mantissa.

Recapitulando os componentes

  • Sinal (primeiro componente): 0para positivo, 1para negativo
  • Expoente (componente do meio): Subtraia 2 (número de bits) - 1 - 1 para obter o verdadeiro expoente
  • Mantissa (último componente): divida por 2 (# de bits) e adicione 1para obter a verdadeira mantissa

Cálculo do número

Juntando todas as três partes, recebemos este número binário:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Que podemos então converter de binário em decimal:

1.1499999999999999 x 2 3 (inexato!)

E multiplique para revelar a representação final do número com o qual começamos ( 9.2) depois de ser armazenado como um valor de ponto flutuante:

9.1999999999999993


Representando como uma fração

9.2

Agora que criamos o número, é possível reconstruí-lo em uma fração simples:

1.0010011001100110011001100110011001100110011001100110 x 10 11

Mude a mantissa para um número inteiro:

10010011001100110011001100110011001100110011001100110 x 10 11-110100

Converter em decimal:

5179139571476070 x 2 3-52

Subtraia o expoente:

5179139571476070 x 2 -49

Transforme expoente negativo em divisão:

5179139571476070/2 49

Multiplicar expoente:

5179139571476070/562949953421312

Qual é igual a:

9.1999999999999993

9,5

>>> float_to_bin_parts(9.5)
['0', '10000000010', '0011000000000000000000000000000000000000000000000000']

Já é possível ver que a mantissa tem apenas 4 dígitos seguidos por muitos zeros. Mas vamos percorrer os passos.

Monte a notação científica binária:

1.0011 x 10 11

Mude o ponto decimal:

10011 x 10 11-100

Subtraia o expoente:

10011 x 10 -1

Binário para decimal:

19 x 2 -1

Expoente negativo para divisão:

19/2 1

Multiplicar expoente:

19/2

É igual a:

9,5



Leitura adicional

mhlester
fonte
1
Há também um bom tutorial que mostra como ir para o outro lado - dada uma representação decimal de um número, como você constrói o equivalente em ponto flutuante. A abordagem de "divisão longa" mostra muito claramente como você termina com um "restante" depois de tentar representar o número. Deve ser adicionado se você quiser ser verdadeiramente "canônico" com sua resposta.
Floris
1
Se você está falando sobre Python e ponto flutuante, sugiro que você inclua pelo menos o tutorial do Python nos seus links: docs.python.org/3.4/tutorial/floatingpoint.html Esse deve ser o único item a ser seguido recurso para problemas de ponto flutuante para programadores Python. Se estiver faltando de alguma forma (e quase certamente está), abra um problema no rastreador de erros do Python para atualizações ou alterações.
Mark Dickinson
@mhlester Se isso for transformado em wiki da comunidade, fique à vontade para incorporar minha resposta à sua.
Nicu Stiurca
5
Definitivamente, essa resposta também deve estar vinculada ao floating-point-gui.de , pois provavelmente é a melhor introdução para iniciantes. Na OMI, ele deve ir além do que "todo cientista da computação deve saber ..." - atualmente, as pessoas que conseguem compreender razoavelmente o artigo de Goldberg já estão bem cientes disso.
Daniel Pryden
1
"Este é um exemplo de uma proporção que pode ser expressa exatamente em binário, mas apenas aproximadamente em decimal". Isso não é verdade. Todos esses rácios 'número com uma potência de dois' são exatos em decimal. Qualquer aproximação é apenas para encurtar o número decimal - por conveniência.
Rick Regan
29

Esta não é uma resposta completa (o mhlester já cobriu muitos bons aspectos que não duplicarei), mas gostaria de enfatizar o quanto a representação de um número depende da base em que você está trabalhando.

Considere a fração 2/3

Na boa e velha base 10, normalmente a escrevemos como algo como

  • 0,666 ...
  • 0,666
  • 0,667

Quando olhamos para essas representações, tendemos a associar cada uma delas à fração 2/3, mesmo que apenas a primeira representação seja matematicamente igual à fração. A segunda e a terceira representações / aproximações apresentam um erro da ordem de 0,001, que na verdade é muito pior que o erro entre 9.2 e 9.1999999999999993. De fato, a segunda representação nem é arredondada corretamente! No entanto, não temos um problema com 0,666 como uma aproximação do número 2/3; portanto, não devemos realmente ter um problema com a aproximação da 9.2 na maioria dos programas . (Sim, em alguns programas é importante.)

Bases numéricas

Então aqui é onde as bases numéricas são cruciais. Se estávamos tentando representar 2/3 na base 3, então

(2/3) 10 = 0,2 3

Em outras palavras, temos uma representação exata e finita para o mesmo número trocando de base! A conclusão é que, embora você possa converter qualquer número em qualquer base, todos os números racionais têm representações finitas exatas em algumas bases, mas não em outras .

Para levar esse ponto para casa, vejamos 1/2. Pode surpreendê-lo que, embora esse número perfeitamente simples tenha uma representação exata na base 10 e 2, ele exija uma representação repetida na base 3.

(1/2) 10 = 0,5 10 = 0,1 2 = 0,1111 ... 3

Por que os números de ponto flutuante são imprecisos?

Como muitas vezes, eles são racionais aproximados que não podem ser representados finitamente na base 2 (os dígitos se repetem) e, em geral, estão aproximando números reais (possivelmente irracionais) que podem não ser representáveis ​​em muitos dígitos finitos em qualquer base.

Nicu Stiurca
fonte
3
Portanto, em outras palavras, a base-3 seria perfeita, 1/3assim como a base-10 é perfeita 1/10. Nenhuma fração funciona em base-2
mhlester
2
@mhlester Sim. E, em geral, a base N é perfeita para qualquer fração cujo denominador é Nou é um múltiplo dele.
Nicu Stiurca
2
E essa é uma das razões pelas quais algumas caixas de ferramentas numéricas controlam "o que foi dividido pelo quê" e, no processo, podem manter a "precisão infinita" para todos os números racionais. Assim como os físicos gostam de manter suas equações simbólicas até o último momento possível, no caso de fatores πetc serem cancelados.
Floris
3
@Floris Também vi casos em que um algoritmo que apenas executa aritmética básica (preserva a racionalidade da entrada), determina se a entrada era (provável) racional, executa a matemática usando a aritmética normal de ponto flutuante e, em seguida, re-estima uma racional aproximação no final para corrigir erros de arredondamento. Em particular, o algoritmo de forma de escalão de linhas reduzidas do Matlab faz isso e ajuda tremendamente a estabilidade numérica.
Nicu Stiurca
@ SchighSchagh - interessante, eu não sabia disso. Eu sei que a estabilidade numérica é algo que não é ensinado suficientemente nestes dias de dupla precisão dupla. O que significa que muitos sentem falta de aprender sobre a elegância de muitos algoritmos bonitos. Eu realmente gosto de algoritmos que calculam e corrigem seus próprios erros.
Floris
13

Embora todas as outras respostas sejam boas, ainda falta uma coisa:

É impossível para representar números irracionais (por exemplo π, sqrt(2), log(3), etc.) precisamente!

E é por isso que eles são chamados irracionais. Nenhuma quantidade de armazenamento de bits no mundo seria suficiente para armazenar um deles. Somente a aritmética simbólica é capaz de preservar sua precisão.

Embora se você limitar suas necessidades matemáticas a números racionais, apenas o problema da precisão se tornará gerenciável. Você precisaria armazenar um par de números inteiros (possivelmente muito grandes) ae bmanter o número representado pela fração a/b. Toda a sua aritmética teria que ser feita em frações, como na matemática do ensino médio (por exemplo a/b * c/d = ac/bd).

Mas é claro que você ainda iria correr para o mesmo tipo de problemas quando pi, sqrt, log, sin, etc. estão envolvidos.

TL; DR

Para aritmética acelerada por hardware, apenas uma quantidade limitada de números racionais pode ser representada. Todo número não representável é aproximado. Alguns números (isto é, irracionais) nunca podem ser representados, não importa o sistema.

LumpN
fonte
4
Curiosamente, existem bases irracionais. Phinary , por exemplo.
Veedrac
5
números irracionais podem ser (apenas) representados em sua base. Por exemplo, pi é 10 na base pi
phuclv
4
O ponto permanece válido: alguns números nunca podem ser representados, independentemente do sistema. Você não ganha nada mudando sua base, porque alguns outros números não podem mais ser representados.
LumpN
4

Existem infinitos números reais (tantos que você não pode enumerá-los) e existem infinitamente muitos números racionais (é possível enumerá-los).

A representação de ponto flutuante é finita (como qualquer coisa em um computador); assim, inevitavelmente, muitos números são impossíveis de representar. Em particular, 64 bits apenas permitem distinguir entre apenas 18.446.744.073.709.551.616 valores diferentes (o que não é nada comparado ao infinito). Com a convenção padrão, 9.2 não é um deles. Os que podem têm a forma m.2 ^ e para alguns números inteiros me.


Você pode criar um sistema de numeração diferente, 10 baseado, por exemplo, em que o 9.2 teria uma representação exata. Mas outros números, digamos 1/3, ainda seriam impossíveis de representar.


Observe também que os números de ponto flutuante de precisão dupla são extremamente precisos. Eles podem representar qualquer número em uma faixa muito ampla, com até 15 dígitos exatos. Para cálculos da vida diária, 4 ou 5 dígitos são mais que suficientes. Você realmente nunca precisará desses 15, a menos que queira contar cada milissegundo de sua vida.

Yves Daoust
fonte
1

Por que não podemos representar 9,2 no ponto flutuante binário?

Os números de ponto flutuante são (simplificando levemente) um sistema de numeração posicional com um número restrito de dígitos e um ponto de raiz móvel.

Uma fração só pode ser expressa exatamente usando um número finito de dígitos em um sistema de numeração posicional se os fatores primos do denominador (quando a fração é expressa em termos mais baixos) são fatores da base.

Os fatores primos de 10 são 5 e 2; portanto, na base 10, podemos representar qualquer fração da forma a / (2 b 5 c ).

Por outro lado, o único fator primo de 2 é 2, portanto, na base 2, podemos representar apenas frações da forma a / (2 b )

Por que os computadores usam essa representação?

Porque é um formato simples de trabalhar e é suficientemente preciso para a maioria dos propósitos. Basicamente, o mesmo motivo pelo qual os cientistas usam a "notação científica" e arredondam seus resultados para um número razoável de dígitos em cada etapa.

Certamente seria possível definir um formato de fração, com (por exemplo) um numerador de 32 bits e um denominador de 32 bits. Seria capaz de representar números que o ponto flutuante de precisão dupla IEEE não poderia, mas igualmente haveria muitos números que podem ser representados no ponto flutuante de precisão dupla que não poderiam ser representados em um formato de fração de tamanho fixo.

No entanto, o grande problema é que esse formato é uma tarefa difícil de fazer cálculos. Por duas razões.

  1. Se você deseja ter exatamente uma representação de cada número, após cada cálculo, é necessário reduzir a fração para os termos mais baixos. Isso significa que, para cada operação, você basicamente precisa fazer um maior cálculo de divisor comum.
  2. Se após o seu cálculo você terminar com um resultado não representável, porque o numerador ou o denominador precisará encontrar o resultado representável mais próximo. Isso não é civil.

Alguns idiomas oferecem tipos de fração, mas geralmente eles fazem isso em combinação com precisão arbitrária, isso evita a necessidade de se preocupar com a aproximação de frações, mas cria seu próprio problema, quando um número passa por um grande número de etapas de cálculo do tamanho do denominador e portanto, o armazenamento necessário para a fração pode explodir.

Alguns idiomas também oferecem tipos de ponto flutuante decimal, sendo usados ​​principalmente em cenários em que é importante que os resultados obtidos pelo computador correspondam às regras de arredondamento pré-existentes que foram escritas com os seres humanos em mente (principalmente cálculos financeiros). É um pouco mais difícil trabalhar com isso do que o ponto flutuante binário, mas o maior problema é que a maioria dos computadores não oferece suporte a hardware.

plugwash
fonte
-4

Tente isto

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String.valueOf(decimalFormat.format(decimalValue))));

' decimalValue' é o seu valor para converter.

Popal
fonte