Por que adicionar 0,1 várias vezes permanece sem perdas?

152

Eu sei que o 0.1número decimal não pode ser representado exatamente com um número binário finito ( explicação ), por double n = 0.1isso perderá alguma precisão e não será exatamente 0.1. Por outro lado, 0.5pode ser representado exatamente porque é 0.5 = 1/2 = 0.1b.

Dito isto, é compreensível que a adição de 0.1 três vezes não dê exatamente, 0.3para que o seguinte código seja impresso false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Mas então, como é que a adição de 0.1 cinco vezes dará exatamente 0.5? O código a seguir é impresso true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Se 0.1não pode ser representado exatamente, como é que adicioná-lo 5 vezes fornece exatamente o 0.5que pode ser representado com precisão?

icza
fonte
7
Se você realmente pesquisar, tenho certeza de que pode descobrir, mas o ponto flutuante é carregado de "surpresas" e, às vezes, é melhor apenas olhar maravilhado.
Hot Licks
3
Você está pensando nisso de uma maneira matemática. A aritmética de ponto flutuante não é matemática de forma alguma.
Jakob,
13
@ HotLicks que é muito a atitude errada de ter.
Hbbs #
2
@RussellBorogove, mesmo que fosse otimizado, apenas seria uma otimização válida se sumtivesse o mesmo valor final como se o loop fosse realmente executado. No padrão C ++, isso é chamado de "regra como se" ou "mesmo comportamento observável".
Hbbs #
7
@ Jakob não é verdade. A aritmética de ponto flutuante é rigorosamente definida, com um bom tratamento matemático dos limites de erro e outros. É que muitos programadores não estão dispostos a seguir adiante na análise ou acreditam erroneamente que "o ponto flutuante é inexato" é tudo o que há para saber e que a análise não vale a pena se preocupar.
Hbbs #

Respostas:

155

O erro de arredondamento não é aleatório e, da maneira como é implementado, tenta minimizar o erro. Isso significa que, às vezes, o erro não é visível ou não há erro.

Por exemplo, 0.1não é exatamente 0.1ou seja, new BigDecimal("0.1") < new BigDecimal(0.1)mas0.5 é, é exatamente1.0/2

Este programa mostra os verdadeiros valores envolvidos.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

impressões

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Nota: isso 0.3está um pouco desligado, mas quando você chega aos 0.4bits, precisa mudar um para baixo para se ajustar ao limite de 53 bits e o erro é descartado. Mais uma vez, um erro volta a aparecer0.6 e 0.7mas para 0.8a 1.0que o erro é descartado.

Adicioná-lo 5 vezes deve acumular o erro, não cancelá-lo.

A razão pela qual há um erro é devido à precisão limitada. ou seja, 53 bits. Isso significa que, à medida que o número usa mais bits à medida que aumenta, os bits precisam ser descartados no final. Isso causa arredondamentos, que neste caso estão a seu favor.
Você pode obter o efeito oposto ao obter um número menor, por exemplo, 0.1-0.0999=>1.0000000000000286E-4 e vê mais erros do que antes.

Um exemplo disso é o porquê no Java 6 Por que Math.round (0.49999999999999994) retorna 1 Nesse caso, a perda de um bit no cálculo resulta em uma grande diferença para a resposta.

Peter Lawrey
fonte
1
Onde isso é implementado?
EpicPandaForce
16
@Zhuinden A CPU segue o padrão IEEE-754. Java fornece acesso às instruções subjacentes da CPU e não se envolve. pt.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@ PeterLawrey: Não necessariamente a CPU. Em uma máquina sem ponto flutuante na CPU (e nenhuma FPU separada em uso), a aritmética IEEE será realizada por software. E se o processamento da CPU tem de ponto flutuante, mas não conforme com os requisitos da IEEE, eu acho que uma implementação Java para esse CPU seria obrigado a usar flutuador macio também ...
R .. GitHub parar de ajudar ICE
1
@R .. Nesse caso, não sei o que aconteceria se você usasse o strictfp Time para considerar números inteiros de ponto fixo, eu acho. (ou BigDecimal)
Peter Lawrey
2
@eugene o principal problema é que os valores limitados que o ponto flutuante pode representar. Essa limitação pode resultar em perda de informações e à medida que o número aumenta, perda de erro. Ele usa arredondamento, mas, neste caso, arredonda para baixo, de modo que o número que seria um pouco grande demais como 0,1 é um pouco grande demais, se transforma no valor correto. Exatamente 0,5
Peter Lawrey
47

Excesso de barramento, em ponto flutuante, x + x + xé exatamente o número do ponto flutuante arredondado corretamente (ou seja, o mais próximo) para o real 3 * x, x + x + x + xé exatamente 4 * xe x + x + x + x + xé novamente a aproximação do ponto flutuante arredondado corretamente para 5 * x.

O primeiro resultado, pois x + x + x, deriva do fato x + xexato.x + x + xé, portanto, o resultado de apenas um arredondamento.

O segundo resultado é mais difícil, uma demonstração dele é discutida aqui (e Stephen Canon alude a outra prova por análise de caso nos últimos 3 dígitos de x). Para resumir, 3 * xestá na mesma bandeja que 2 * xou na mesma bandeja que 4 *x e, em cada caso, é possível deduzir que o erro na terceira adição cancela o erro na segunda adição (o sendo a primeira adição exata, como já dissemos).

O terceiro resultado, " x + x + x + x + xé arredondado corretamente", deriva do segundo da mesma maneira que o primeiro deriva da exatidão de x + x.


O segundo resultado explica por que 0.1 + 0.1 + 0.1 + 0.1exatamente é o número de ponto flutuante 0.4: os números racionais 1/10 e 4/10 são aproximados da mesma maneira, com o mesmo erro relativo, quando convertidos em ponto flutuante. Esses números de ponto flutuante têm uma proporção de exatamente 4 entre eles. O primeiro e o terceiro resultado mostram que, 0.1 + 0.1 + 0.1e 0.1 + 0.1 + 0.1 + 0.1 + 0.1pode-se esperar que tenha menos erro do que pode ser inferido pela análise de erro ingênua, mas, por si só, eles relacionam os resultados apenas com respectivamente 3 * 0.1e 5 * 0.1, que se espera que sejam próximos, mas não necessariamente idênticos aos 0.3e0.5 .

Se você continuar adicionando 0.1após a quarta adição, finalmente observará erros de arredondamento que fazem com que “ 0.1adicionado a si n vezes” divergam dos binários por um tempo. Depois disso, a absorção começaria a ocorrer e a curva ficaria plana.n * 0.1 e ainda mais do n / 10. Se você plotar os valores de "0,1 adicionado a si mesmo n vezes" em função de n, você observaria linhas de inclinação constante por binades (assim que o resultado da enésima adição for destinado a cair em uma binada específica, pode-se esperar que as propriedades da adição sejam semelhantes às adições anteriores que produziram um resultado na mesma binade). Dentro de uma mesma caixa, o erro aumentará ou diminuirá. Se você olhasse para a sequência das pistas de binade a binade, reconheceria os dígitos repetidos de0.1

Pascal Cuoq
fonte
1
Na primeira linha, você está dizendo que x + x + x está exatamente correto, mas a partir do exemplo da pergunta não está.
alboz
2
@ Alboz Eu digo que x + x + xé exatamente o número de ponto flutuante arredondado corretamente para o real 3 * x. "Arredondado corretamente" significa "o mais próximo" neste contexto.
Pascal Cuoq 30/09
4
+1 Esta deve ser a resposta aceita. Na verdade, oferece explicações / provas do que está acontecendo, em vez de apenas generalidades vagas.
R .. GitHub Pare de ajudar o gelo
1
@Alboz (tudo isso é previsto pela pergunta). Mas o que esta resposta explica é como os erros são cancelados por sorte, em vez de se acumularem na pior das hipóteses.
Hbbs #
1
@chebus 0.1 é 0x1.9999999999999999999999… p-4 em hexadecimal (uma sequência infinita de dígitos). É aproximado em precisão dupla como 0x1.99999ap-4. 0,2 é 0x1,999999999999999999999… p-3 em hexadecimal. Pelo mesmo motivo que 0.1 é aproximado como 0x1.99999ap-4, 0.2 é aproximado como 0x1.99999ap-3. Enquanto isso, 0x1.99999ap-3 também é exatamente 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Os sistemas de ponto flutuante fazem várias magias, incluindo ter alguns bits extras de precisão para arredondar. Assim, o erro muito pequeno devido à representação inexata de 0,1 acaba sendo arredondado para 0,5.

Pense no ponto flutuante como uma maneira excelente, mas INEXATA de representar números. Nem todos os números possíveis são facilmente representados em um computador. Números irracionais como PI. Ou como SQRT (2). (Os sistemas matemáticos simbólicos podem representá-los, mas eu disse "facilmente".)

O valor do ponto flutuante pode ser extremamente próximo, mas não exato. Pode estar tão perto que você pode navegar para Plutão e sair em milímetros. Mas ainda não é exato no sentido matemático.

Não use ponto flutuante quando precisar ser exato e não aproximado. Por exemplo, os aplicativos de contabilidade desejam acompanhar exatamente um determinado número de centavos em uma conta. Inteiros são bons para isso porque são exatos. O principal problema que você precisa observar com números inteiros é o estouro.

O uso de BigDecimal para moeda funciona bem porque a representação subjacente é um número inteiro, embora grande.

Reconhecendo que os números de ponto flutuante são inexatos, eles ainda têm muitos usos. Sistemas de coordenadas para navegação ou coordenadas em sistemas gráficos. Valores astronômicos. Valores científicos. (Você provavelmente não pode saber a massa exata de uma bola de beisebol dentro da massa de um elétron, de modo que a inexatidão realmente não importa.)

Para aplicativos de contagem (incluindo contabilidade), use inteiro. Para contar o número de pessoas que passam por um portão, use int ou long.

DannyB
fonte
2
A pergunta está marcada com [java]. A definição da linguagem Java não fornece "alguns bits extras de precisão", apenas alguns bits extras de expoente (e isso é apenas se você não usar strictfp). Só porque você renunciou a entender algo não significa que é insondável nem que outros devam renunciar a entendê-lo. Veja stackoverflow.com/questions/18496560 como um exemplo dos comprimentos que as implementações de Java irão para implementar a definição de linguagem (que não inclui nenhuma provisão para bits de precisão extra nem, com strictfp, para nenhum bit de exp extra)
Pascal Cuoq