A matemática do ponto flutuante está quebrada?

2983

Considere o seguinte código:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Por que essas imprecisões acontecem?

Cato Johnston
fonte
127
Variáveis ​​de ponto flutuante normalmente têm esse comportamento. É causado pela maneira como eles são armazenados no hardware. Para mais informações, consulte o artigo da Wikipedia sobre números de ponto flutuante .
217 Ben Ben
62
O JavaScript trata os decimais como números de ponto flutuante , o que significa que operações como adição podem estar sujeitas a erros de arredondamento. Você pode querer dar uma olhada neste artigo: O que cada cientista computador deve saber sobre Floating-Point Arithmetic
matt b
4
Apenas para informação, TODOS os tipos numéricos em javascript são IEEE-754 Doubles.
Gary Willoughby
6
Como o JavaScript usa o padrão IEEE 754 para Math, ele usa números flutuantes de 64 bits . Isso causa erros de precisão ao fazer cálculos de ponto flutuante (decimal), em resumo, devido a computadores trabalhando na Base 2, enquanto o decimal é Base 10 .
Pardeep Jain

Respostas:

2253

A matemática binária de ponto flutuante é assim. Na maioria das linguagens de programação, ela é baseada no padrão IEEE 754 . O ponto crucial do problema é que os números são representados nesse formato como um número inteiro vezes a potência de dois; números racionais (como 0.1, o que é 1/10) cujo denominador não é um poder de dois não podem ser exatamente representados.

No formato 0.1padrão binary64, a representação pode ser escrita exatamente como

Por outro lado, o número racional 0.1, ou seja 1/10, pode ser escrito exatamente como

  • 0.1 em decimal ou
  • 0x1.99999999999999...p-4em um análogo da notação hexadecimal C99, em que o ...representa uma sequência interminável de 9's.

As constantes 0.2e 0.3no seu programa também serão aproximações aos seus valores verdadeiros. Acontece que o mais próximo doubleque 0.2é maior do que o número racional 0.2, mas que o mais próximo doubleque 0.3é menor do que o número racional 0.3. A soma de 0.1e 0.2acaba sendo maior que o número racional 0.3e, portanto, discordando da constante em seu código.

Um tratamento bastante abrangente das questões aritméticas de ponto flutuante é o que todo cientista da computação deve saber sobre aritmética de ponto flutuante . Para uma explicação mais fácil de digerir, consulte floating-point-gui.de .

Nota lateral: Todos os sistemas de números posicionais (base-N) compartilham esse problema com precisão

Números decimais antigos simples (base 10) têm os mesmos problemas, e é por isso que números como 1/3 acabam como 0,333333333 ...

Você acabou de encontrar um número (3/10) que é fácil de representar com o sistema decimal, mas não se encaixa no sistema binário. Também funciona nos dois sentidos (em algum grau pequeno): 1/16 é um número feio em decimal (0,0625), mas em binário parece tão puro quanto um 10.000º em decimal (0,0001) ** - se estivéssemos em o hábito de usar um sistema numérico de base 2 em nossas vidas diárias, você até olha para esse número e instintivamente entende que pode chegar lá pela metade de algo, pela metade de novo e de novo e de novo.

** É claro que não é exatamente assim que os números de ponto flutuante são armazenados na memória (eles usam uma forma de notação científica). No entanto, ilustra o ponto em que erros binários de precisão de ponto flutuante tendem a surgir porque os números do "mundo real" com os quais geralmente estamos interessados ​​em trabalhar são frequentemente potências de dez - mas apenas porque usamos um sistema de números decimais dia- hoje. É também por isso que diremos coisas como 71% em vez de "5 em cada 7" (71% é uma aproximação, pois 5/7 não pode ser representado exatamente com qualquer número decimal).

Portanto, não: números binários de ponto flutuante não são quebrados, eles são tão imperfeitos quanto qualquer outro sistema de números de base N :)

Side Side Note: Trabalhando com flutuadores na programação

Na prática, esse problema de precisão significa que você precisa usar funções de arredondamento para arredondar seus números de ponto flutuante para o número de casas decimais em que estiver interessado antes de exibi-los.

Você também precisa substituir os testes de igualdade por comparações que permitam certa tolerância, o que significa:

Você não fazerif (x == y) { ... }

Em vez disso, faça if (abs(x - y) < myToleranceValue) { ... }.

onde absé o valor absoluto. myToleranceValueprecisa ser escolhido para sua aplicação específica - e terá muito a ver com a quantidade de "espaço de manobra" que você está preparado para permitir e qual será o maior número que você comparará (devido a problemas de perda de precisão ) Cuidado com as constantes de estilo "epsilon" no seu idioma preferido. Estes não devem ser usados ​​como valores de tolerância.

Daniel Scott
fonte
181
Eu acho que "alguma constante de erro" é mais correta que "The Epsilon" porque não há "The Epsilon" que poderia ser usado em todos os casos. Épsilons diferentes precisam ser usados ​​em diferentes situações. E a máquina epsilon quase nunca é uma boa constante para usar.
Rotsor
34
Não é bem verdade que toda a matemática de ponto flutuante é baseada no padrão IEEE [754]. Ainda existem alguns sistemas em uso que possuem o antigo FP hexadecimal da IBM, por exemplo, e ainda existem placas gráficas que não suportam a aritmética IEEE-754. É verdade uma aproximação razoável, no entanto.
Stephen Canon
19
Cray abandonou a conformidade com a IEEE-754 para obter velocidade. O Java também diminuiu sua aderência como otimização.
Art Taylor
28
Eu acho que você deve adicionar algo a esta resposta sobre como os cálculos sobre dinheiro devem sempre, sempre ser feitos com aritmética de ponto fixo em números inteiros , porque o dinheiro é quantizado. (Pode fazer sentido fazer cálculos contábeis internos em pequenas frações de centavo ou qualquer que seja sua menor unidade monetária - isso geralmente ajuda a, por exemplo, reduzir o erro de arredondamento ao converter "US $ 29,99 por mês" em uma taxa diária - mas deve ainda ser aritmética de ponto fixo).
Zwol
18
Fato interessante: esse valor de 0,1 não estar exatamente representado no ponto flutuante binário causou um infame bug do software de mísseis Patriot que resultou em 28 pessoas mortas durante a primeira guerra do Iraque.
HDL
603

Perspectiva de um designer de hardware

Acredito que devo acrescentar a perspectiva de um designer de hardware a isso, pois projeto e construo hardware de ponto flutuante. Conhecer a origem do erro pode ajudar a entender o que está acontecendo no software e, finalmente, espero que isso ajude a explicar as razões pelas quais erros de ponto flutuante acontecem e parecem se acumular ao longo do tempo.

1. Visão Geral

Do ponto de vista da engenharia, a maioria das operações de ponto flutuante terá algum elemento de erro, já que o hardware que faz os cálculos de ponto flutuante só precisa ter um erro inferior a metade de uma unidade em último lugar. Portanto, muito hardware será interrompido com uma precisão necessária apenas para gerar um erro inferior a metade de uma unidade em último lugar para uma única operação, o que é especialmente problemático na divisão de ponto flutuante. O que constitui uma única operação depende de quantos operandos a unidade leva. Para a maioria, são dois, mas algumas unidades aceitam 3 ou mais operandos. Por esse motivo, não há garantia de que operações repetidas resultem em um erro desejável, pois os erros aumentam com o tempo.

2. Padrões

A maioria dos processadores segue o padrão IEEE-754 , mas alguns usam padrões não normalizados ou diferentes. Por exemplo, há um modo desnormalizado no IEEE-754 que permite a representação de números de ponto flutuante muito pequenos à custa da precisão. A seguir, no entanto, abordaremos o modo normalizado do IEEE-754, que é o modo típico de operação.

No padrão IEEE-754, os projetistas de hardware têm qualquer valor de erro / epsilon contanto que seja menos da metade de uma unidade em último lugar, e o resultado deve ser apenas menos da metade de uma unidade no último local para uma operação. Isso explica por que, quando há operações repetidas, os erros são somados. Para a precisão dupla IEEE-754, esse é o 54º bit, já que 53 bits são usados ​​para representar a parte numérica (normalizada), também chamada de mantissa, do número de ponto flutuante (por exemplo, o 5.3 em 5.3e5). As próximas seções entram em mais detalhes sobre as causas do erro de hardware em várias operações de ponto flutuante.

3. Causa do erro de arredondamento na divisão

A principal causa do erro na divisão de ponto flutuante são os algoritmos de divisão usados ​​para calcular o quociente. A maioria dos sistemas computacionais calcula a divisão usando multiplicação por uma inversa, principalmente em Z=X/Y,Z = X * (1/Y). Uma divisão é computada iterativamente, ou seja, cada ciclo calcula alguns bits do quociente até que a precisão desejada seja alcançada, o que para IEEE-754 é qualquer coisa com um erro menor que uma unidade em último lugar. A tabela de recíprocos de Y (1 / Y) é conhecida como tabela de seleção de quociente (QST) na divisão lenta, e o tamanho em bits da tabela de seleção de quociente geralmente é a largura do radical ou um número de bits de o quociente calculado em cada iteração, mais alguns bits de guarda. Para o padrão IEEE-754, precisão dupla (64 bits), seria o tamanho da raiz do divisor, além de alguns bits de proteção k, onde k>=2. Assim, por exemplo, uma Tabela de Seleção de Quociente típica para um divisor que calcula 2 bits do quociente por vez (raiz 4) seria 2+2= 4bits (mais alguns bits opcionais).

3.1 Erro de arredondamento de divisão: aproximação de recíproca

O que os recíprocos existem na tabela de seleção de quociente dependem do método de divisão : divisão lenta, como a divisão SRT, ou divisão rápida, como a divisão Goldschmidt; cada entrada é modificada de acordo com o algoritmo de divisão na tentativa de gerar o menor erro possível. Em qualquer caso, porém, todos os recíprocos são aproximaçõesrecíproco real e introduzir algum elemento de erro. Os métodos de divisão lenta e de divisão rápida calculam o quociente de forma iterativa, ou seja, um número de bits do quociente é calculado a cada etapa; o resultado é subtraído do dividendo e o divisor repete as etapas até que o erro seja menor que a metade de um. unidade em último lugar. Os métodos de divisão lenta calculam um número fixo de dígitos do quociente em cada etapa e geralmente são mais baratos de construir, e os métodos de divisão rápida calculam um número variável de dígitos por etapa e geralmente são mais caros de construir. A parte mais importante dos métodos de divisão é que a maioria deles se baseia na multiplicação repetida por uma aproximação de um recíproco, de modo que eles são propensos a erros.

4. Erros de arredondamento em outras operações: truncamento

Outra causa dos erros de arredondamento em todas as operações são os diferentes modos de truncamento da resposta final que o IEEE-754 permite. Há truncado, arredondado para zero, arredondado para o mais próximo (padrão), arredondado para baixo e arredondado. Todos os métodos introduzem um elemento de erro inferior a uma unidade em último lugar para uma única operação. Com o tempo e operações repetidas, o truncamento também adiciona cumulativamente o erro resultante. Esse erro de truncamento é especialmente problemático na exponenciação, que envolve alguma forma de multiplicação repetida.

5. Operações Repetidas

Como o hardware que faz os cálculos de ponto flutuante precisa apenas produzir um resultado com um erro inferior a metade de uma unidade em último lugar para uma única operação, o erro aumentará ao longo de operações repetidas se não for observado. Essa é a razão pela qual, em cálculos que exigem um erro limitado, os matemáticos usam métodos como o dígito par arredondar para o mais próximo no último local da IEEE-754, porque, com o tempo, os erros têm mais probabilidade de se cancelar. para fora, e Intervalo Aritmética combinado com as variações de IEEE 754 arredondamento modosprever erros de arredondamento e corrigi-los. Devido ao seu erro relativo baixo em comparação com outros modos de arredondamento, o arredondamento para o dígito par mais próximo (em último lugar) é o modo de arredondamento padrão do IEEE-754.

Observe que o modo de arredondamento padrão, dígito par arredondar para o mais próximo , garante um erro menor que a metade de uma unidade no último local para uma operação. O uso de truncamento, arredondamento e arredondamento sozinho pode resultar em um erro maior que a metade de uma unidade em último lugar, mas menor que uma unidade em último lugar, portanto, esses modos não são recomendados, a menos que sejam usado na aritmética de intervalos.

6. Resumo

Em suma, a razão fundamental dos erros nas operações de ponto flutuante é uma combinação do truncamento no hardware e o truncamento de um recíproco no caso de divisão. Como o padrão IEEE-754 requer apenas um erro inferior a metade de uma unidade em último lugar para uma única operação, os erros de ponto flutuante em operações repetidas serão adicionados, a menos que sejam corrigidos.

KernelPanik
fonte
8
(3) está errado. O erro de arredondamento em uma divisão não é inferior a uma unidade em último lugar, mas no máximo meia unidade em último lugar.
precisa saber é o seguinte
6
@ gnasher729 Boa captura. A maioria das operações básicas também possui um erro inferior a 1/2 de uma unidade, usando o modo de arredondamento IEEE padrão. Editou a explicação e também observou que o erro pode ser maior que 1/2 de uma ulp, mas menor que 1 ulp se o usuário substituir o modo de arredondamento padrão (isso é especialmente verdadeiro nos sistemas incorporados).
KernelPanik
39
(1) Os números de ponto flutuante não apresentam erro. Todo valor de ponto flutuante é exatamente o que é. A maioria das operações de ponto flutuante (mas não todas) fornece resultados inexatos. Por exemplo, não há valor de ponto flutuante binário exatamente igual a 1.0 / 10.0. Algumas operações (por exemplo, 1,0 + 1,0) não dar resultados exatos, por outro lado.
Solomon Slow
19
"A principal causa do erro na divisão de ponto flutuante, são os algoritmos de divisão usados ​​para calcular o quociente" é uma coisa muito enganadora de se dizer. Para uma divisão em conformidade com a IEEE-754, a única causa de erro na divisão de ponto flutuante é a incapacidade do resultado ser representado exatamente no formato do resultado; o mesmo resultado é calculado independentemente do algoritmo usado.
Stephen Canon
6
@ Matt Desculpe pela resposta tardia. É basicamente devido a problemas de recursos / tempo e compensações. Existe uma maneira de fazer divisão longa / mais divisão 'normal', chamada Divisão SRT com base dois. No entanto, isso muda e subtrai repetidamente o divisor do dividendo e leva muitos ciclos de clock, pois calcula apenas um bit do quociente por ciclo de clock. Usamos tabelas de recíprocos para poder calcular mais bits do quociente por ciclo e fazer trocas efetivas de desempenho / velocidade.
KernelPanik
463

Ao converter .1 ou 1/10 para a base 2 (binária), você obtém um padrão de repetição após o ponto decimal, assim como tenta representar 1/3 na base 10. O valor não é exato e, portanto, você não pode fazer matemática exata usando métodos normais de ponto flutuante.

Joel Coehoorn
fonte
133
Ótima e curta resposta. Repetindo aparência padrão como 0,00011001100110011001100110011001100110011001100110011 ...
Konstantin Chernov
4
Isso não explica por que não é usado um algoritmo melhor que não se converta em binários em primeiro lugar.
Dmitri Zaitsev
12
Porque desempenho. O uso do binário é milhares de vezes mais rápido, porque é nativo para a máquina.
Joel Coehoorn
7
Existem métodos que produzem valores decimais exatos. BCD (decimal codificado binário) ou várias outras formas de número decimal. No entanto, ambos são mais lentos (muito mais lentos) e exigem mais armazenamento do que o ponto flutuante binário. (por exemplo, o BCD compactado armazena 2 dígitos decimais em um byte. São 100 valores possíveis em um byte que podem realmente armazenar 256 valores possíveis, ou 100/256, que desperdiçam cerca de 60% dos valores possíveis de um byte.)
Duncan C
16
@Jacksonkr você ainda está pensando na base 10. Os computadores são de base 2.
Joel Coehoorn
307

A maioria das respostas aqui aborda essa questão em termos técnicos muito secos. Eu gostaria de abordar isso em termos que os seres humanos normais possam entender.

Imagine que você está tentando fatiar pizzas. Você tem um cortador de pizza robótico que pode cortar fatias de pizza exatamente ao meio. Pode cortar pela metade uma pizza inteira ou pela metade uma fatia existente, mas, em qualquer caso, a metade é sempre exata.

Esse cortador de pizza tem movimentos muito finos, e se você começar com uma pizza inteira, reduza pela metade e continue cortando pela metade a menor fatia de cada vez, você pode fazer a metade pela metade 53 vezes antes que a fatia seja muito pequena para suas habilidades de alta precisão . Nesse ponto, você não pode mais dividir pela metade essa fatia muito fina, mas deve incluí-la ou excluí-la como está.

Agora, como você colocaria todas as fatias de maneira a adicionar um décimo (0,1) ou um quinto (0,2) de uma pizza? Realmente pense sobre isso e tente resolvê-lo. Você pode até tentar usar uma pizza de verdade, se tiver um cortador de pizza de precisão mítico à mão. :-)


Os programadores mais experientes, é claro, sabem a resposta real, que é que não há maneira de juntar um décimo ou quinto exato da pizza usando essas fatias, por mais finas que sejam. Você pode fazer uma aproximação muito boa e, se somar a aproximação de 0,1 com a aproximação de 0,2, obterá uma boa aproximação de 0,3, mas ainda assim é apenas uma aproximação.

Para números de precisão dupla (que é a precisão que permite reduzir pela metade a sua pizza 53 vezes), os números imediatamente menores e maiores que 0,1 são 0,09999999999999999167332731531132594682276248931884765625 e 0.1000000000000000055511151231257827021181583404541015625. O último está um pouco mais próximo de 0,1 do que o primeiro, portanto, um analisador numérico, com uma entrada de 0,1, favorece o último.

(A diferença entre esses dois números é a "menor fatia" que devemos decidir incluir, que introduz um viés para cima ou exclui, que introduz um viés para baixo. O termo técnico para essa menor fatia é uma ulp .)

No caso de 0,2, os números são todos iguais, apenas aumentados por um fator de 2. Novamente, favorecemos o valor ligeiramente superior a 0,2.

Observe que em ambos os casos, as aproximações para 0.1 e 0.2 têm um leve viés ascendente. Se adicionarmos um número suficiente desses vieses, eles empurrarão o número cada vez mais longe do que queremos e, de fato, no caso de 0,1 + 0,2, o viés é alto o suficiente para que o número resultante não seja mais o número mais próximo para 0,3.

Em particular, 0.1 + 0.2 é realmente 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000000444089209850062616169452699759999999999999999999999999999999999999999999999999999999999999999999999999999999999999999099099


PS Algumas linguagens de programação também fornecem cortadores de pizza que podem dividir fatias em décimos exatos . Embora esses cortadores de pizza sejam incomuns, se você tiver acesso a um, use-o quando for importante obter exatamente um décimo ou um quinto de uma fatia.

(Originalmente publicado no Quora.)

Chris Jester-Young
fonte
3
Observe que existem alguns idiomas que incluem matemática exata. Um exemplo é Scheme, por exemplo, via GNU Guile. Veja draketo.de/english/exact-math-to-the-rescue - eles mantêm a matemática como frações e apenas fatiam no final.
Arne Babenhauserheide
5
@FloatingRock Na verdade, muito poucas linguagens de programação convencionais possuem números racionais incorporados. Arne é um Schemer, como eu sou, então são coisas que nos estragam.
Chris Jester-Young
5
@ArneBabenhauserheide Acho que vale a pena acrescentar que isso só funcionará com números racionais. Portanto, se você estiver fazendo contas com números irracionais como pi, precisará armazená-lo como um múltiplo de pi. Obviamente, qualquer cálculo envolvendo pi não pode ser representado como um número decimal exato.
Aidiakapi 11/0315
13
@connexo Okay. Como você programa seu rotador de pizza para obter 36 graus? Quanto é 36 graus? (Dica: se você pode definir isso da maneira exata, também possui um cortador de pizza com fatias e décimo exato.) Em outras palavras, não é possível ter 1/360 (um grau) ou 1 / 10 (36 graus) com apenas ponto flutuante binário.
Chris Jester-Young
12
@connexo Além disso, "todo idiota" não pode girar uma pizza exatamente 36 graus. Os seres humanos são propensos a erros demais para fazer algo tão preciso.
Chris Jester-Young
212

Erros de arredondamento de ponto flutuante. 0.1 não pode ser representado com precisão na base-2 como na base-10 devido ao fator primo ausente de 5. Assim como 1/3 leva um número infinito de dígitos para representar em decimal, mas é "0,1" na base-3, 0.1 pega um número infinito de dígitos na base 2, onde não na base 10. E os computadores não têm uma quantidade infinita de memória.

Devin Jeanpierre
fonte
133
computadores não precisa de uma quantidade infinita de memória para obter 0,1 + 0,2 = 0,3 certo
Pacerier
23
@Pacerier Claro, eles poderiam usar dois números inteiros de precisão ilimitada para representar uma fração ou usar notação de cotação. É a noção específica de "binário" ou "decimal" que torna isso impossível - a idéia de que você tem uma sequência de dígitos binários / decimais e, em algum lugar, um ponto de raiz. Para obter resultados racionais precisos, precisaríamos de um formato melhor.
Devin Jeanpierre 15/10
15
@ Pacerier: nem o ponto flutuante binário nem o decimal podem armazenar com precisão 1/3 ou 1/13. Os tipos decimais de ponto flutuante podem representar com precisão valores da forma M / 10 ^ E, mas são menos precisos que números de ponto flutuante binários de tamanho semelhante quando se trata de representar a maioria das outras frações . Em muitas aplicações, é mais útil ter maior precisão com frações arbitrárias do que ter precisão perfeita com algumas "especiais".
Supercat
13
@Pacerier Eles fazem isso se estiverem armazenando os números como carros alegóricos binários, que foi o ponto da resposta.
Mark Amery
3
@chux: A diferença de precisão entre os tipos binários e decimais não é enorme, mas a diferença de 10: 1 na melhor ou na pior das hipóteses para os tipos decimais é muito maior que a diferença de 2: 1 nos tipos binários. Estou curioso para saber se alguém construiu hardware ou software escrito para operar com eficiência em qualquer um dos tipos decimais, pois nenhum deles parece passível de implementação eficiente em hardware nem software.
Supercat
121

Além das outras respostas corretas, convém dimensionar seus valores para evitar problemas com a aritmética de ponto flutuante.

Por exemplo:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... ao invés de:

var result = 0.1 + 0.2;     // result === 0.3 returns false

A expressão 0.1 + 0.2 === 0.3retorna falseem JavaScript, mas felizmente a aritmética inteira em ponto flutuante é exata, portanto, erros de representação decimal podem ser evitados pelo dimensionamento.

Como exemplo prático, para evitar problemas de ponto flutuante onde a precisão é primordial, é recomendável 1 manipular o dinheiro como um número inteiro representando o número de centavos: 2550centavos em vez de 25.50dólares.


1 Douglas Crockford: JavaScript: As peças boas : Apêndice A - Peças horríveis (página 105) .

Daniel Vassallo
fonte
3
O problema é que a conversão em si é imprecisa. 16,08 * 100 = 1607.9999999999998. Temos que recorrer à divisão do número e à conversão separadamente (como em 16 * 100 + 08 = 1608)?
Jason
38
A solução aqui é fazer todos os seus cálculos em número inteiro, depois dividir pela sua proporção (100 neste caso) e arredondar apenas quando apresentar os dados. Isso garantirá que seus cálculos sejam sempre precisos.
David Granado
16
Apenas para escolher um pouco: a aritmética inteira é exata apenas no ponto flutuante até um ponto (trocadilho intencional). Se o número for maior que 0x1p53 (para usar a notação de ponto flutuante hexadecimal do Java 7, = 9007199254740992), a ulp é 2 nesse ponto e, portanto, 0x1p53 + 1 é arredondado para 0x1p53 (e 0x1p53 + 3 é arredondado para 0x1p53 + 4, por causa do arredondamento para o mesmo). :-D Mas certamente, se o seu número for menor que 9 quadrilhões, você deve ficar bem. :-P
Chris Jester-Young
2
Jason, você deve arredondar o resultado (int) (16,08 * 100 + 0,5) #
Mikhail Semenov
@CodyBugstein " Então, como você obtém .1 + .2 para mostrar .3? " Escreva uma função de impressão personalizada para colocar o decimal no local desejado.
RonJohn
113

Minha resposta é bastante longa, então eu a dividi em três seções. Como a questão é sobre matemática de ponto flutuante, enfatizei o que a máquina realmente faz. Também especifiquei a precisão dupla (64 bits), mas o argumento se aplica igualmente a qualquer aritmética de ponto flutuante.

Preâmbulo

Um número de formato de ponto flutuante binário de precisão dupla IEEE 754 (binary64) representa um número no formato

valor = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

em 64 bits:

  • O primeiro bit é o bit de sinal : 1se o número for negativo, 0caso contrário 1 .
  • Os próximos 11 bits são o expoente , que é compensado por 1023. Em outras palavras, depois de ler os bits do expoente de um número de precisão dupla, 1023 deve ser subtraído para obter a potência de dois.
  • Os 52 bits restantes são o significando (ou mantissa). Na mantissa, um 'implícito' 1.é sempre 2 omitido, já que o bit mais significativo de qualquer valor binário é 1.

1 - O IEEE 754 permite o conceito de um zero assinado - +0e -0são tratados de maneira diferente: 1 / (+0)é infinito positivo; 1 / (-0)é infinito negativo. Para valores zero, os bits mantissa e expoente são zero. Nota: os valores zero (+0 e -0) não são explicitamente classificados como desnormais 2 .

2 - Este não é o caso de números desnormais , que possuem um expoente de deslocamento igual a zero (e um implícito 0.). O intervalo de números de precisão dupla denormal é d min ≤ | x | ≤ d max , onde d min (o menor número diferente de zero representável) é 2 -1.023-51 (≈ 4,94 * 10 -324 ) e d max (o maior número denormal, para o qual a mantissa consiste inteiramente em 1s) é 2 -1023 + 1 - 2 - 1023 - 51 (~ 2,225 * 10 - 308 ).


Transformando um número de precisão dupla em binário

Existem muitos conversores online para converter um número de ponto flutuante de precisão dupla em binário (por exemplo, em binaryconvert.com ), mas aqui está um código C # de amostra para obter a representação IEEE 754 para um número de precisão dupla (eu separo as três partes com dois pontos ( :) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Chegando ao ponto: a pergunta original

(Pule para o final da versão TL; DR)

Cato Johnston (o autor da pergunta) perguntou por que 0,1 + 0,2! = 0,3.

Escritas em binário (com dois pontos separando as três partes), as representações IEEE 754 dos valores são:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Observe que a mantissa é composta por dígitos recorrentes de 0011. Esta é a chave para porque é que há qualquer erro para os cálculos - 0,1, 0,2 e 0,3 não pode ser representada em binário precisamente em um finito número de bits binários não mais do que 1/9, 1/3 ou 1/7 pode ser representado com precisão em dígitos decimais .

Observe também que podemos diminuir a potência no expoente em 52 e deslocar o ponto na representação binária para a direita em 52 lugares (semelhante a 10 -3 * 1,23 == 10 -5 * 123). Isso então nos permite representar a representação binária como o valor exato que ela representa na forma a * 2 p . onde 'a' é um número inteiro.

Converter os expoentes em decimal, remover o deslocamento e adicionar novamente o implícito 1(entre colchetes) 0,1 e 0,2 são:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Para adicionar dois números, o expoente precisa ser o mesmo, ou seja:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Como a soma não tem a forma 2 n * 1. {bbb}, aumentamos o expoente em um e mudamos o ponto decimal ( binário ) para obter:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Agora existem 53 bits na mantissa (o 53º está entre colchetes na linha acima). O padrão modo de arredondamento para IEEE 754 é ' Round to mais próxima ' - ou seja, se um número x cai entre dois valores de um e b , o valor onde o bit menos significativo é zero é escolhido.

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Note-se que um e b diferem apenas no último bit; ...0011+ 1= ...0100. Nesse caso, o valor com o bit menos significativo de zero é b , portanto, a soma é:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

considerando que a representação binária de 0,3 é:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

que difere apenas da representação binária da soma de 0,1 e 0,2 por 2 -54 .

A representação binária de 0.1 e 0.2 são as representações mais precisas dos números permitidos pelo IEEE 754. A adição dessas representações, devido ao modo de arredondamento padrão, resulta em um valor que difere apenas no bit menos significativo.

TL; DR

Escrevendo 0.1 + 0.2em uma representação binária IEEE 754 (com dois pontos separando as três partes) e comparando-a 0.3, isto é (coloquei os bits distintos entre colchetes):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Convertidos de volta para decimal, esses valores são:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

A diferença é exatamente 2 -54 , que é ~ 5,5511151231258 × 10 -17 - insignificante (para muitas aplicações) quando comparada aos valores originais.

Comparar os últimos bits de um número de ponto flutuante é inerentemente perigoso, como quem ler o famoso " O que todo cientista da computação deve saber sobre aritmética de ponto flutuante " (que cobre todas as principais partes desta resposta) saberá.

A maioria das calculadoras usar adicionais dígitos de guarda de contornar este problema, que é como 0.1 + 0.2daria 0.3: os poucos bits finais são arredondados.

Wai Ha Lee
fonte
14
Minha resposta foi rejeitada logo após publicá-la. Desde então, fiz muitas alterações (inclusive anotando explicitamente os bits recorrentes ao escrever 0,1 e 0,2 em binário, que eu havia omitido no original). Com a chance de o eleitor que vê isso, você poderia me dar algum feedback para que eu possa melhorar minha resposta? Sinto que minha resposta acrescenta algo novo, já que o tratamento da soma no IEEE 754 não é coberto da mesma maneira em outras respostas. Embora "O que todo cientista da computação deva saber ..." cubra o mesmo material, minha resposta lida especificamente com o caso de 0,1 + 0,2.
Wai Ha Lee
57

Os números de ponto flutuante armazenados no computador consistem em duas partes, um número inteiro e um expoente para o qual a base é levada e multiplicada pela parte inteira.

Se o computador estivesse trabalhando na base 10, 0.1seria 1 x 10⁻¹, 0.2seria 2 x 10⁻¹e 0.3seria 3 x 10⁻¹. A matemática de números inteiros é fácil e exata, portanto a adição 0.1 + 0.2obviamente resultará em 0.3.

Os computadores geralmente não funcionam na base 10, eles funcionam na base 2. Você ainda pode obter resultados exatos para alguns valores, por exemplo, 0.5é 1 x 2⁻¹e 0.25é 1 x 2⁻², e adicioná-los em 3 x 2⁻², ou 0.75. Exatamente.

O problema vem com números que podem ser representados exatamente na base 10, mas não na base 2. Esses números precisam ser arredondados para o equivalente mais próximo. Assumindo o formato muito comum de ponto flutuante IEEE de 64 bits, o número mais próximo de 0.1é 3602879701896397 x 2⁻⁵⁵e o número mais próximo de 0.2é 7205759403792794 x 2⁻⁵⁵; adicionando-os resulta em 10808639105689191 x 2⁻⁵⁵, ou um valor decimal exato de 0.3000000000000000444089209850062616169452667236328125. Os números de ponto flutuante geralmente são arredondados para exibição.

Mark Ransom
fonte
2
@ Mark Obrigado por esta explicação clara, mas surge a pergunta por que 0,1 + 0,4 corresponde exatamente a 0,5 (pelo menos em Python 3). Além disso, qual é a melhor maneira de verificar a igualdade ao usar flutuadores no Python 3?
pchegoor
2
@ user2417881 As operações de ponto flutuante IEEE têm regras de arredondamento para todas as operações e, às vezes, o arredondamento pode produzir uma resposta exata mesmo quando os dois números estão um pouco desativados. Os detalhes são muito longos para um comentário e, de qualquer maneira, não sou especialista neles. Como você vê nesta resposta, 0,5 é um dos poucos decimais que podem ser representados em binário, mas isso é apenas uma coincidência. Para teste de igualdade, consulte stackoverflow.com/questions/5595425/… .
Mark Ransom
1
@ user2417881 sua pergunta me intrigou então eu transformou-o em uma pergunta completa e resposta: stackoverflow.com/q/48374522/5987
Mark Ransom
47

Erro de arredondamento do ponto flutuante. Do que todo cientista da computação deve saber sobre aritmética de ponto flutuante :

Comprimir infinitamente muitos números reais em um número finito de bits requer uma representação aproximada. Embora existam infinitos números inteiros, na maioria dos programas o resultado de cálculos inteiros pode ser armazenado em 32 bits. Por outro lado, dado qualquer número fixo de bits, a maioria dos cálculos com números reais produzirá quantidades que não podem ser representadas exatamente usando tantos bits. Portanto, o resultado de um cálculo de ponto flutuante geralmente deve ser arredondado para se ajustar novamente à sua representação finita. Esse erro de arredondamento é o recurso característico da computação em ponto flutuante.

Brett Daniel
fonte
33

Minha solução alternativa:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

precisão refere-se ao número de dígitos que você deseja preservar após o ponto decimal durante a adição.

Justineo
fonte
30

Muitas respostas boas foram postadas, mas eu gostaria de acrescentar mais uma.

Nem todos os números podem ser representados por flutuadores / duplos. Por exemplo, o número "0,2" será representado como "0,200000003" com precisão única no padrão de ponto de flutuação IEEE754.

O modelo para números reais da loja sob o capô representa números flutuantes como

insira a descrição da imagem aqui

Mesmo que você possa digitar 0.2facilmente, FLT_RADIXe DBL_RADIXé 2; não 10 para um computador com FPU que use "Padrão IEEE para aritmética binária de ponto flutuante (ISO / IEEE Std 754-1985)".

Portanto, é um pouco difícil representar exatamente esses números. Mesmo se você especificar esta variável explicitamente sem nenhum cálculo intermediário.

bruziuz
fonte
28

Algumas estatísticas relacionadas a essa famosa questão de precisão dupla.

Ao adicionar todos os valores ( a + b ) usando uma etapa de 0,1 (de 0,1 a 100), temos ~ 15% de chance de erro de precisão . Observe que o erro pode resultar em valores um pouco maiores ou menores. aqui estão alguns exemplos:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

Ao subtrair todos os valores ( a - b onde a> b ) usando uma etapa de 0,1 (de 100 a 0,1), temos ~ 34% de chance de erro de precisão . aqui estão alguns exemplos:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% e 34% são realmente enormes; portanto, sempre use BigDecimal quando a precisão for de grande importância. Com 2 dígitos decimais (etapa 0,01), a situação piora um pouco mais (18% e 36%).

Kostas Chalkias
fonte
28

Não, não está quebrado, mas a maioria das frações decimais deve ser aproximada

Sumário

A aritmética de ponto flutuante é exata, infelizmente, não combina bem com a nossa representação habitual de números da base 10, portanto, geralmente estamos fornecendo uma entrada um pouco diferente do que escrevemos.

Mesmo números simples como 0,01, 0,02, 0,03, 0,04 ... 0,24 não são representáveis ​​exatamente como frações binárias. Se você contar 0,01, 0,02, 0,03 ..., até chegar a 0,25, você obterá a primeira fração representável na base 2 . Se você tentasse usar o FP, seu 0,01 teria sido um pouco desligado; portanto, a única maneira de adicionar 25 deles a um bom exato 0,25 exigiria uma longa cadeia de causalidade envolvendo bits de proteção e arredondamento. É difícil prever, então levantamos as mãos e dizemos "FP é inexato", mas isso não é verdade.

Damos constantemente ao hardware FP algo que parece simples na base 10, mas que é uma fração repetida na base 2.

Como isso aconteceu?

Quando escrevemos em decimal, toda fração (especificamente, todo decimal que termina) é um número racional da forma

           a / (2 n x 5 m )

Em binário, obtemos apenas o termo 2 n , ou seja:

           a / 2 n

Assim, em decimal, não podemos representar 1 / 3 . Como a base 10 inclui 2 como fator primordial, todo número que podemos escrever como uma fração binária também pode ser escrito como uma fração base 10. No entanto, quase tudo o que escrevemos como uma fração de base 10 é representável em binário. No intervalo de 0,01, 0,02, 0,03 ... 0,99, apenas três números podem ser representados em nosso formato FP: 0,25, 0,50 e 0,75, porque são 1/4, 1/2 e 3/4, todos os números com um fator primordial usando apenas o termo 2 n .

Na base de 10 nós não pode representar 1 / 3 . Mas em binário, não podemos fazer 1 / 10 ou 1 / 3 .

Portanto, embora toda fração binária possa ser escrita em decimal, o inverso não é verdadeiro. E, de fato, a maioria das frações decimais se repete em binário.

Lidando com isso

Os desenvolvedores geralmente são instruídos a fazer comparações <épsilon , o melhor conselho pode ser arredondar para valores integrais (na biblioteca C: round () e roundf (), ou seja, permanecer no formato FP) e depois comparar. O arredondamento para uma fração decimal específica resolve a maioria dos problemas com a saída.

Além disso, em problemas reais de processamento de números (os problemas para os quais o FP foi inventado em computadores antigos e terrivelmente caros), as constantes físicas do universo e todas as outras medições são conhecidas apenas por um número relativamente pequeno de números significativos; portanto, todo o espaço do problema era "inexato" de qualquer maneira. A "precisão" do FP não é um problema nesse tipo de aplicativo.

Todo o problema realmente surge quando as pessoas tentam usar o FP para a contagem de feijões. Funciona para isso, mas apenas se você se apegar a valores integrais, o que meio que anula o ponto de usá-lo. É por isso que temos todas essas bibliotecas de software de fração decimal.

Adoro a resposta da Pizza por Chris , porque ela descreve o problema real, não apenas a habitual sensação de "imprecisão". Se o FP fosse simplesmente "impreciso", poderíamos consertar isso e o teríamos feito décadas atrás. A razão pela qual não temos isso é porque o formato FP é compacto e rápido e é a melhor maneira de processar muitos números. Além disso, é um legado da era espacial e da corrida armamentista e tentativas iniciais de resolver grandes problemas com computadores muito lentos usando pequenos sistemas de memória. (Às vezes, núcleos magnéticos individuais para armazenamento de 1 bit, mas isso é outra história. )

Conclusão

Se você está apenas contando beans em um banco, as soluções de software que usam representações de string decimal em primeiro lugar funcionam perfeitamente. Mas você não pode fazer cromodinâmica quântica ou aerodinâmica dessa maneira.

DigitalRoss
fonte
Arredondar para o número inteiro mais próximo não é uma maneira segura de resolver o problema de comparação em todos os casos. 0,4999998 e 0,500001 arredondam para números inteiros diferentes, portanto há uma "zona de perigo" em torno de cada ponto de corte de arredondamento. (Eu sei que aquelas cordas decimais provavelmente não são exatamente representável como carros alegóricos IEEE binários.)
Peter Cordes
1
Além disso, mesmo que o ponto flutuante seja um formato "legado", ele é muito bem projetado. Não sei de nada que alguém mudaria se o redesenhasse agora. Quanto mais eu aprendo, mais eu acho que é realmente bem projetado. por exemplo, o expoente tendencioso significa que flutuadores binários consecutivos têm representações inteiras consecutivas, para que você possa implementar nextafter()com um incremento ou decréscimo inteiro na representação binária de um flutuador IEEE. Além disso, você pode comparar carros alegóricos como números inteiros e obter a resposta certa, exceto quando ambos são negativos (por causa da magnitude do sinal versus o complemento de 2).
Peter Cordes
Eu discordo, os carros alegóricos devem ser armazenados como decimais e não binários e todos os problemas são resolvidos.
Ronen Festinger
" X / (2 ^ n + 5 ^ n) não deveria ser" x / (2 ^ n * 5 ^ n) "?
Wai Ha Lee
@RonenFestinger - e quanto a 1/3?
Stephen C
19

Você tentou a solução de fita adesiva?

Tente determinar quando os erros ocorrem e corrija-os com instruções if curtas, não é bonito, mas para alguns problemas é a única solução e essa é uma delas.

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

Eu tive o mesmo problema em um projeto de simulação científica em c # e posso dizer que, se você ignorar o efeito borboleta, ele se transformará em um grande dragão gordo e o morderá no a **

workoverflow
fonte
19

Para oferecer a melhor solução , posso dizer que descobri o seguinte método:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Deixe-me explicar por que é a melhor solução. Como outros mencionados nas respostas acima, é uma boa ideia usar a função Javascript toFixed () pronta para usar para resolver o problema. Mas provavelmente você encontrará alguns problemas.

Imagine que você está indo para somar dois números flutuar como 0.2e 0.7aqui está: 0.2 + 0.7 = 0.8999999999999999.

O resultado esperado foi 0.9que significa que você precisa de um resultado com precisão de 1 dígito neste caso. Portanto, você deveria ter usado, (0.2 + 0.7).tofixed(1) mas não pode simplesmente fornecer um determinado parâmetro para toFixed (), pois depende do número fornecido, por exemplo

`0.22 + 0.7 = 0.9199999999999999`

Neste exemplo, você precisa de 2 dígitos de precisão toFixed(2), portanto, qual deve ser o parâmetro para caber em cada número flutuante?

Você pode dizer que seja 10 em todas as situações:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Droga! O que você vai fazer com esses zeros indesejados depois das 9? É hora de convertê-lo para flutuar para fazê-lo como você deseja:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Agora que você encontrou a solução, é melhor oferecê-la como uma função como esta:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Vamos tentar você mesmo:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Você pode usá-lo desta maneira:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Como o W3SCHOOLS sugere que também existe outra solução, você pode multiplicar e dividir para resolver o problema acima:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

Lembre-se de que (0.2 + 0.1) * 10 / 10não funcionará, embora pareça o mesmo! Prefiro a primeira solução, pois posso aplicá-la como uma função que converte a flutuação de entrada em flutuante de saída precisa.

Mohammad Musavi
fonte
isso me fez uma verdadeira dor de cabeça. Soma 12 números flutuantes e mostro a soma e a média desses números. usar toFixed () pode corrigir a soma de 2 números, mas quando soma vários números, o salto é significativo.
Nuryagdy Mustapayev 17/03
@Nuryagdy Mustapayev Eu não entendi sua intenção, pois testei antes que você possa somar 12 números flutuantes, use a função floatify () no resultado e faça o que quiser, não observei nenhum problema ao usá-lo.
Mohammad Musavi 18/03
Só estou dizendo na minha situação que tenho cerca de 20 parâmetros e 20 fórmulas em que o resultado de cada fórmula depende de outras, essa solução não ajudou.
Nuryagdy Mustapayev 20/03
16

Esses números estranhos aparecem porque os computadores usam o sistema de números binários (base 2) para fins de cálculo, enquanto usamos o decimal (base 10).

Há uma maioria de números fracionários que não podem ser representados precisamente em binário ou decimal ou em ambos. Resultado - Um número arredondado (mas preciso) resulta.

Piyush S528
fonte
Não entendo o seu segundo parágrafo.
Nae
1
@Nae Gostaria de traduzir o segundo parágrafo como "A maioria das frações não pode ser representada exatamente em decimal ou binário. Portanto, a maioria dos resultados será arredondada - embora ainda sejam precisos para o número de bits / dígitos inerentes à representação. sendo usado."
Steve Summit
15

Muitas das numerosas duplicatas dessa pergunta perguntam sobre os efeitos do arredondamento de ponto flutuante em números específicos. Na prática, é mais fácil ter uma idéia de como isso funciona, observando os resultados exatos dos cálculos de interesse, e não apenas lendo sobre ele. Algumas linguagens fornecem maneiras de fazer isso - como converter um floatou doublepara BigDecimalem Java.

Como essa é uma questão independente da linguagem, ela precisa de ferramentas independentes da linguagem, como um conversor de ponto decimal para ponto flutuante .

Aplicando-o aos números da pergunta, tratados como duplos:

0.1 converte para 0.1000000000000000055511151231257827021181583404541015625,

0,2 converte para 0,200000000000000011102230246251565404236316680908203125,

0,3 converte para 0,299999999999999988897769753748434595763683319091796875 e

0.30000000000000004 converte para 0.3000000000000000444089209850062616169452667236328125.

A adição manual dos dois primeiros números ou em uma calculadora decimal, como a Calculadora de precisão total , mostra que a soma exata das entradas reais é 0,3000000000000000166533453693773481063544750213623046875.

Se fosse arredondado para o equivalente a 0,3, o erro de arredondamento seria 0,0000000000000000277555756156289135105907917022705078125. O arredondamento para o equivalente a 0,30000000000000004 também fornece erro de arredondamento 0.0000000000000000277555756156289135105907917022705078125. O desempate de arredondamento para o par se aplica.

Retornando ao conversor de ponto flutuante, o hexadecimal bruto para 0,30000000000000004 é 3fd3333333333334, que termina em um dígito par e, portanto, é o resultado correto.

Patricia Shanahan
fonte
2
Para a pessoa cuja edição eu acabei de reverter: considero citações de código apropriadas para citar códigos. Esta resposta, por ser neutra em termos de idioma, não contém nenhum código citado. Os números podem ser usados ​​em frases em inglês e isso não os transforma em código.
Patricia Shanahan
Este é provavelmente porque alguém formatado seus números como código - não para formatação, mas para facilitar a leitura.
Wai Ha Lee
... também, a rodada para mesmo se refere à representação binária , não à representação decimal . Veja isso ou, por exemplo, isso .
Wai Ha Lee
@WaiHaLee Eu não apliquei o teste par / ímpar em nenhum número decimal, apenas hexadecimal. Um dígito hexadecimal é uniforme se, e somente se, o bit menos significativo de sua expansão binária for zero.
Patricia Shanahan
14

Dado que ninguém mencionou isso ...

Algumas linguagens de alto nível, como Python e Java, vêm com ferramentas para superar as limitações binárias de ponto flutuante. Por exemplo:

  • decimalMódulo do Python e BigDecimalclasse do Java , que representam números internamente com notação decimal (em oposição à notação binária). Ambos têm precisão limitada, portanto ainda propensos a erros, mas resolvem os problemas mais comuns com aritmética binária de ponto flutuante.

    Os decimais são muito bons quando se lida com dinheiro: dez centavos mais vinte centavos são sempre exatamente trinta centavos:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    O decimalmódulo do Python é baseado no padrão IEEE 854-1987 .

  • fractionsMódulo do Python e BigFractionclasse do Apache Common . Ambos representam números racionais como (numerator, denominator)pares e podem fornecer resultados mais precisos que a aritmética decimal de ponto flutuante.

Nenhuma dessas soluções é perfeita (especialmente se observarmos as performances ou se precisarmos de uma precisão muito alta), mas elas ainda resolvem um grande número de problemas com a aritmética binária de ponto flutuante.

Andrea Corbellini
fonte
14

Posso apenas adicionar; as pessoas sempre assumem que este é um problema do computador, mas se você contar com as mãos (base 10), não poderá obtê- (1/3+1/3=2/3)=truelo, a menos que tenha infinito para adicionar 0,333 ... a 0,333 ... assim como no (1/10+2/10)!==3/10problema da base 2, você o trunca para 0,333 + 0,333 = 0,666 e provavelmente o arredonda para 0,667, o que também seria tecnicamente impreciso.

Contar no ternário, e terços não são um problema - talvez alguma corrida com 15 dedos em cada mão pergunte por que sua matemática decimal foi quebrada ...


fonte
Como os humanos usam números decimais, não vejo boas razões para que os carros alegóricos não sejam representados como um decimal por padrão, para que tenhamos resultados precisos.
Ronen Festinger
Os seres humanos usam muitos outros do que a base 10 (decimais) bases, binário sendo o que usamos mais para calcular .. o 'boa razão' é que você simplesmente não pode representar cada fração em cada base ..
A aritmética binária do @RonenFestinger é fácil de implementar em computadores porque requer apenas oito operações básicas com dígitos: digamos $ a $, $ b $ em $ 0,1 $ tudo que você precisa saber é $ \ operatorname {xor} (a, b) $ e $ \ nome do operador {cb} (a, b) $, em que xor é exclusivo ou e cb é o "bit de transporte" que é $ 0 $ em todos os casos, exceto quando $ a = 1 = b $; nesse caso, temos um (na verdade, a comutatividade de todas as operações economiza US $ 2 $ casos e tudo o que você precisa é de US $ 6 $ regras). A expansão decimal precisa que casos de $ 10 \ vezes 11 $ (em notação decimal) sejam armazenados e estados diferentes de $ 10 $ para cada bit e desperdice o armazenamento no transporte.
Oskar Limka
@RonenFestinger - O decimal não é mais preciso. É isso que esta resposta está dizendo. Para qualquer base que você escolher, haverá números racionais (frações) que fornecerão seqüências de dígitos com repetições infinitas. Para o registro, alguns dos primeiros computadores fez uso base 10 representações para números, mas os pioneiros designers de hardware de computador concluiu logo que a base 2 era muito mais fácil e mais eficiente de implementar.
Stephen C
9

O tipo de matemática de ponto flutuante que pode ser implementada em um computador digital usa necessariamente uma aproximação dos números reais e operações neles. (A versão padrão tem mais de cinquenta páginas de documentação e tem um comitê para lidar com suas erratas e aperfeiçoamentos adicionais.)

Essa aproximação é uma mistura de aproximações de diferentes tipos, cada uma das quais pode ser ignorada ou cuidadosamente contabilizada devido à sua maneira específica de desvio da exatidão. Também envolve vários casos excepcionais explícitos, tanto no nível de hardware quanto de software, que a maioria das pessoas passa direto, fingindo não perceber.

Se você precisar de precisão infinita (usando o número π, por exemplo, em vez de um de seus muitos períodos mais curtos), escreva ou use um programa matemático simbólico.

Mas se você concorda com a idéia de que, às vezes, a matemática de ponto flutuante é confusa em valor, a lógica e os erros podem se acumular rapidamente, e você pode escrever seus requisitos e testes para permitir isso, seu código pode frequentemente se adaptar ao que está em sua FPU.

Blair Houghton
fonte
9

Apenas por diversão, brinquei com a representação de carros alegóricos, seguindo as definições do padrão C99 e escrevi o código abaixo.

O código imprime a representação binária de carros alegóricos em 3 grupos separados

SIGN EXPONENT FRACTION

e depois imprime uma soma que, quando somada com precisão suficiente, mostrará o valor que realmente existe no hardware.

Portanto, quando você escreve float x = 999..., o compilador transformará esse número em uma representação de bits impressa pela função, de xxmodo que a soma impressa pela função yyseja igual ao número fornecido.

Na realidade, essa soma é apenas uma aproximação. Para o número 999.999.999, o compilador inserirá na representação em bits do número flutuante o número 1.000.000.000

Após o código, anexo uma sessão do console, na qual calculo a soma dos termos para as duas constantes (menos PI e 999999999) que realmente existem no hardware, inseridas pelo compilador.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Aqui está uma sessão de console em que eu calculo o valor real do flutuador que existe no hardware. Eu costumava bcimprimir a soma dos termos gerados pelo programa principal. Pode-se inserir essa soma em python replou algo semelhante também.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

É isso aí. O valor de 999999999 é de fato

999999999.999999446351872

Você também pode verificar se bc-3,14 também está perturbado. Não se esqueça de definir um scalefator bc.

A soma exibida é o que está dentro do hardware. O valor que você obtém calculando depende da escala que você definir. Eu defini o scalefator como 15. Matematicamente, com precisão infinita, parece que é 1.000.000.000.

alinsoar
fonte
5

Outra maneira de ver isso: Utilizados são 64 bits para representar números. Como conseqüência, não há mais que 2 ** 64 = 18.446.744.073.709.551.616 números diferentes podem ser representados com precisão.

No entanto, Math diz que já existem infinitas casas decimais entre 0 e 1. O IEE 754 define uma codificação para usar esses 64 bits com eficiência para um espaço numérico muito maior, mais NaN e +/- Infinito, para que haja lacunas entre os números representados com precisão preenchidos com números apenas aproximados.

Infelizmente 0,3 fica em uma lacuna.

Torsten Becker
fonte
4

Imagine trabalhar na base dez com, digamos, 8 dígitos de precisão. Você verifica se

1/3 + 2 / 3 == 1

e saiba que isso retorna false. Por quê? Bem, como números reais, temos

1/3 = 0,333 .... e 2/3 = 0,666 ....

Truncando em oito casas decimais, obtemos

0.33333333 + 0.66666666 = 0.99999999

que é, obviamente, diferente de 1.00000000exatamente 0.00000001.


A situação para números binários com um número fixo de bits é exatamente análoga. Como números reais, temos

1/10 = 0,0001100110011001100 ... (base 2)

e

1/5 = 0,0011001100110011001 ... (base 2)

Se os truncássemos para, digamos, sete bits, teríamos

0.0001100 + 0.0011001 = 0.0100101

enquanto, por outro lado,

3/10 = 0,01001100110011 ... (base 2)

que, truncado para sete bits, é 0.0100110, e esses diferem exatamente 0.0000001.


A situação exata é um pouco mais sutil, porque esses números geralmente são armazenados em notação científica. Então, por exemplo, em vez de armazenar 1/10, 0.0001100podemos armazená-lo como algo parecido 1.10011 * 2^-4, dependendo de quantos bits alocamos para o expoente e a mantissa. Isso afeta quantos dígitos de precisão você obtém para seus cálculos.

O resultado é que, devido a esses erros de arredondamento, você basicamente nunca deseja usar == em números de ponto flutuante. Em vez disso, você pode verificar se o valor absoluto da diferença é menor do que algum número pequeno fixo.

Daniel McLaury
fonte
4

Desde o Python 3.5, você pode usar a math.isclose()função para testar a igualdade aproximada:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
nauer
fonte
3

Como esse segmento ramificou-se um pouco em uma discussão geral sobre as implementações atuais de ponto flutuante, eu acrescentaria que existem projetos para corrigir seus problemas.

Dê uma olhada em https://posithub.org/, por exemplo, que mostra um tipo de número chamado posit (e seu antecessor unum) que promete oferecer melhor precisão com menos bits. Se meu entendimento estiver correto, também corrigirá o tipo de problemas na pergunta. Projeto bastante interessante, a pessoa por trás disso é um matemático, Dr. John Gustafson . A coisa toda é de código aberto, com muitas implementações reais em C / C ++, Python, Julia e C # ( https://hastlayer.com/arithmetics ).

Piedone
fonte
3

Na verdade, é bem simples. Quando você tem um sistema de base 10 (como o nosso), ele pode expressar apenas frações que usam um fator primordial da base. Os fatores primos de 10 são 2 e 5. Portanto, 1/2, 1/4, 1/5, 1/8 e 1/10 podem ser expressos de maneira limpa, porque todos os denominadores usam fatores primos de 10. Em contraste, 1 / 3, 1/6 e 1/7 são decimais repetidos porque seus denominadores usam um fator primo de 3 ou 7. Em binário (ou base 2), o único fator primo é 2. Portanto, você só pode expressar frações de maneira limpa que contém apenas 2 como fator primordial. Em binário, 1/2, 1/4, 1/8 seriam todos expressos de maneira limpa como decimais. Enquanto 1/5 ou 1/10 estaria repetindo decimais. Portanto, 0,1 e 0,2 (1/10 e 1/5), enquanto decimais limpos em um sistema base 10, estão repetindo decimais no sistema base 2 em que o computador está operando. Quando você faz contas com esses decimais repetidos,

From https ://0.30000000000000004.com/

Vlad Agurets
fonte
3

Os números decimais, tais como 0.1, 0.2e 0.3não estão representados exatamente em binário codificado tipos de ponto flutuante. A soma das aproximações 0.1e 0.2difere da aproximação usada 0.3, daí a falsidade de0.1 + 0.2 == 0.3 como pode ser vista mais claramente aqui:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Resultado:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Para que esses cálculos sejam avaliados com mais confiabilidade, você precisará usar uma representação baseada em decimal para valores de ponto flutuante. O Padrão C não especifica esses tipos por padrão, mas como uma extensão descrita em um relatório técnico .

Os _Decimal32, _Decimal64e _Decimal128tipos podem estar disponíveis no seu sistema (por exemplo, GCC suporta-los em alvos selecionados , mas Clang não apoiá-los no OS X ).

chqrlie
fonte
1

Math.sum (javascript) .... tipo de substituição de operador

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

a ideia é usar os operadores Math, para evitar erros de flutuação

Math.sum detecta automaticamente a precisão a ser usada

Math.sum aceita qualquer número de argumentos

Bortunac
fonte
1
Não tenho certeza se você respondeu à pergunta " Por que essas imprecisões acontecem? ".
Wai Ha Lee
de uma forma você está certo, mas eu vim aqui de um comportamento estranho javascript sobre este assunto ... eu só quero compartilhar tipo de solução
bortunac
Você ainda não está respondendo à pergunta.
Wai Ha Lee
k você tem um problema com isso ... me diga para onde movê-lo ou se você insistir, eu posso excluí-lo
bortunac
0

Acabei de ver esta questão interessante em torno de pontos flutuantes:

Considere os seguintes resultados:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Podemos ver claramente um ponto de interrupção quando 2**53+1- tudo funciona bem até 2**53.

>>> (2**53) - int(float(2**53))
0

Digite a descrição da imagem aqui

Isso acontece devido ao formato de ponto flutuante binário de dupla precisão IEEE 754: binary64

Na página da Wikipedia para o formato de ponto flutuante de precisão dupla :

O ponto flutuante binário de precisão dupla é um formato comumente usado em PCs, devido à sua maior variedade sobre o ponto flutuante de precisão única, apesar de seu desempenho e custo de largura de banda. Como no formato de ponto flutuante de precisão única, ele não possui precisão em números inteiros quando comparado com um formato inteiro do mesmo tamanho. É comumente conhecido simplesmente como duplo. O padrão IEEE 754 especifica um binary64 como tendo:

  • Bit de sinal: 1 bit
  • Expoente: 11 bits
  • Precisão significativa: 53 bits (52 armazenados explicitamente)

Digite a descrição da imagem aqui

O valor real assumido por um dado de dupla precisão de 64 bits com um determinado expoente tendencioso e uma fração de 52 bits é

Digite a descrição da imagem aqui

ou

Digite a descrição da imagem aqui

Obrigado a @a_guest por me indicar isso.

costargc
fonte
-1

Uma pergunta diferente foi nomeada como duplicada para esta:

Em C ++, por que o resultado é cout << xdiferente do valor que um depurador está mostrando parax ?

O xna questão é umafloat variável.

Um exemplo seria

float x = 9.9F;

O depurador mostra 9.89999962, a saída da coutoperação é9.9 .

A resposta acaba sendo a coutprecisão padrão parafloat 6, então arredonda para 6 dígitos decimais.

Veja aqui para referência


fonte
1
IMO - postar isso aqui foi a abordagem errada. Sei que é frustrante, mas as pessoas que precisam de uma resposta para a pergunta original (aparentemente agora excluída!) Não a encontrarão aqui. Se você realmente acha que seu trabalho merece ser salvo, sugiro: 1) procurar outro Q que isso realmente responda, 2) criar uma pergunta auto-respondida.
Stephen C