Por que Math.round (0.49999999999999994) retorna 1?

567

No programa a seguir, você pode ver que cada valor um pouco menor que o .5arredondado, exceto por 0.5.

for (int i = 10; i >= 0; i--) {
    long l = Double.doubleToLongBits(i + 0.5);
    double x;
    do {
        x = Double.longBitsToDouble(l);
        System.out.println(x + " rounded is " + Math.round(x));
        l--;
    } while (Math.round(x) > i);
}

impressões

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 1
0.4999999999999999 rounded is 0

Estou usando o Java 6, atualização 31.

Peter Lawrey
fonte
1
Em java 1.7.0 funciona ok i.imgur.com/hZeqx.png
Coffee
2
@ Adel: veja meu comentário sobre a resposta de Oli , parece que o Java 6 implementa isso (e documenta isso ) de uma maneira que pode causar uma perda adicional de precisão adicionando 0.5ao número e depois usando floor; O Java 7 não o documenta mais dessa maneira (provavelmente / provavelmente porque eles o corrigiram).
TJ Crowder
1
Foi um bug em um programa de teste que escrevi. ;)
Peter Lawrey
1
Outro exemplo que mostra valores de ponto flutuante não pode ser tomado pelo valor de face.
Michaël Roy
1
Depois de pensar sobre isso. Não vejo problema. 0,49999999999999994 é maior que o menor número representável menor que 0,5, e a representação na forma decimal legível por humanos é uma aproximação que está tentando nos enganar.
Michaël Roy

Respostas:

574

Sumário

No Java 6 (e presumivelmente antes), round(x)é implementado como floor(x+0.5). 1 Este é um bug de especificação, exatamente para este caso patológico. 2 O Java 7 não exige mais essa implementação interrompida. 3

O problema

0,5 + 0,49999999999999994 é exatamente 1 em dupla precisão:

static void print(double d) {
    System.out.printf("%016x\n", Double.doubleToLongBits(d));
}

public static void main(String args[]) {
    double a = 0.5;
    double b = 0.49999999999999994;

    print(a);      // 3fe0000000000000
    print(b);      // 3fdfffffffffffff
    print(a+b);    // 3ff0000000000000
    print(1.0);    // 3ff0000000000000
}

Isso ocorre porque 0.49999999999999994 tem um expoente menor que 0,5; portanto, quando são adicionados, a mantissa é alterada e o ULP fica maior.

A solução

Desde o Java 7, o OpenJDK (por exemplo) o implementa da seguinte maneira: 4

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) // greatest double value less than 0.5
        return (long)floor(a + 0.5d);
    else
        return 0;
}

1. http://docs.oracle.com/javase/6/docs/api/java/lang/Math.html#round%28double%29

2. http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6430675 (créditos para @SimonNickerson por encontrar isso)

3. http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round%28double%29

4. http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/java/lang/Math.java#Math.round%28double%29

Oliver Charlesworth
fonte
Não vejo essa definição roundno Javadoc paraMath.round ou na visão geral da Mathclasse.
TJ Crowder
3
@ Oli: Oh, agora isso é interessante, eles escolheram o Java 7 (os documentos aos quais vinculei) - talvez para evitar causar esse tipo de comportamento estranho, provocando uma (mais) perda de precisão.
TJ Crowder
@TJCrowder: Sim, é interessante. Você sabe se existe algum tipo de documento "notas de versão" / "melhorias" para versões individuais de Java, para que possamos verificar essa suposição?
precisa
6
@MohammadFadin: Veja, por exemplo, en.wikipedia.org/wiki/Single_precision e en.wikipedia.org/wiki/Unit_in_the_last_place .
26768 Oliver
1
Não posso deixar de pensar que essa correção é apenas cosmética, pois o zero é mais visível. Não há dúvida de muitos outros valores de ponto flutuante afetados por esse erro de arredondamento.
Michaël Roy
83

Código fonte no JDK 6:

public static long round(double a) {
    return (long)Math.floor(a + 0.5d);
}

Código fonte no JDK 7:

public static long round(double a) {
    if (a != 0x1.fffffffffffffp-2) {
        // a is not the greatest double value less than 0.5
        return (long)Math.floor(a + 0.5d);
    } else {
        return 0;
    }
}

Quando o valor é 0,49999999999999994d, no JDK 6, ele chamará floor e, portanto, retornará 1, mas no JDK 7, a ifcondição está verificando se o número é o maior valor duplo menor que 0,5 ou não. Como neste caso o número não é o maior valor duplo menor que 0,5, o elsebloco retorna 0.

Você pode tentar 0.49999999999999999d, que retornará 1, mas não 0, porque esse é o maior valor duplo menor que 0,5.

Chandra Sekhar
fonte
o que acontece com 1.499999999999999994 aqui, então? retorna 2? ele deve retornar 1, mas isso gera o mesmo erro que anteriormente, mas com um 1.?
mmm
6
1.499999999999999994 não pode ser representado em ponto flutuante de precisão dupla. 1.4999999999999998 é o menor dobro menor que 1,5. Como você pode ver na pergunta, o floormétodo o arredonda corretamente.
29412 OrangeDog
26

Eu tenho o mesmo no JDK 1.6 de 32 bits, mas no Java 7 de 64 bits, tenho 0 para 0,49999999999999994, que arredondado é 0 e a última linha não é impressa. Parece ser um problema de VM, no entanto, usando pontos flutuantes, você deve esperar que os resultados diferam um pouco em vários ambientes (CPU, modo de 32 ou 64 bits).

E, ao usar roundou inverter matrizes, etc., esses bits podem fazer uma enorme diferença.

saída x64:

10.5 rounded is 11
10.499999999999998 rounded is 10
9.5 rounded is 10
9.499999999999998 rounded is 9
8.5 rounded is 9
8.499999999999998 rounded is 8
7.5 rounded is 8
7.499999999999999 rounded is 7
6.5 rounded is 7
6.499999999999999 rounded is 6
5.5 rounded is 6
5.499999999999999 rounded is 5
4.5 rounded is 5
4.499999999999999 rounded is 4
3.5 rounded is 4
3.4999999999999996 rounded is 3
2.5 rounded is 3
2.4999999999999996 rounded is 2
1.5 rounded is 2
1.4999999999999998 rounded is 1
0.5 rounded is 1
0.49999999999999994 rounded is 0
Marinheiro Danubiano
fonte
No Java 7 (a versão que você está usando para testá-lo), o bug foi corrigido.
Iván Pérez
1
Eu acho que você quis dizer 32 bits. Duvido que o pt.wikipedia.org/wiki/ZEBRA_%28computer%29 possa executar Java e duvido que tenha havido uma máquina de 33 bits desde então.
CHX
@chx obviamente, porque eu escrevi 32 bit antes :)
Danubian Sailor
11

A resposta a seguir é um trecho de um relatório de bug do Oracle 6430675 em. Visite o relatório para obter a explicação completa.

Os métodos {Math, StrictMath.round são operacionalmente definidos como

(long)Math.floor(a + 0.5d)

para argumentos duplos. Embora essa definição geralmente funcione conforme o esperado, ela fornece o resultado surpreendente de 1, em vez de 0, para 0x1.fffffffffffffp-2 (0.49999999999999994).

O valor 0,49999999999999994 é o maior valor de ponto flutuante menor que 0,5. Como um literal de ponto flutuante hexadecimal, seu valor é 0x1.fffffffffffffp-2, que é igual a (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Portanto, o valor exato da soma

(0.5 - 2^54) + 0.5

é 1 - 2 ^ 54. Isso está na metade do caminho entre os dois números adjacentes de ponto flutuante (1 - 2 ^ 53) e 1. No modo aritmético IEEE 754 de arredondamento para o mais próximo, usado pelo Java, quando um resultado de ponto flutuante é inexato, o mais próximo dos dois valores de ponto flutuante representáveis ​​que suportam o resultado exato devem ser retornados; se ambos os valores estiverem igualmente próximos, aquele cujo último bit zero será retornado. Nesse caso, o valor de retorno correto da adição é 1, e não o maior valor menor que 1.

Enquanto o método está operando conforme definido, o comportamento nessa entrada é muito surpreendente; a especificação pode ser alterada para algo mais como "Arredondar para o comprimento mais próximo, arredondar as amarras", o que permitiria mudar o comportamento dessa entrada.

shiv.mymail
fonte