Pregunta ¿Por qué Math.round (0.49999999999999994) devuelve 1?


En el siguiente programa, puede ver que cada valor es ligeramente menor que .5 se redondea hacia abajo, excepto 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);
}

huellas dactilares

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

Estoy usando Java 6 actualización 31.


530
2018-03-28 07:30


origen


Respuestas:


Resumen

En Java 6 (y presumiblemente antes), round(x) se implementa como floor(x+0.5).1   Este es un error de especificación, precisamente para este caso patológico.2  Java 7 ya no exige esta implementación interrumpida.3

El problema

0.5 + 0.49999999999999994 es exactamente 1 en doble precisión:

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
}

Esto se debe a que 0.49999999999999994 tiene un exponente más pequeño que 0.5, por lo que cuando se agregan, su mantisa se desplaza y la ULP aumenta.

La solución

Desde Java 7, OpenJDK (por ejemplo) lo implementa así: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 a @SimonNickerson por encontrar esto)

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


546
2018-03-28 07:38



Esto parece ser un error conocido (Error Java 6430675: Math.round tiene un comportamiento sorprendente para 0x1.fffffffffffffp-2) que se ha corregido en Java 7.


228
2018-03-28 07:53



Código fuente en JDK 6:

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

Código fuente en 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;
    }
}

Cuando el valor es 0.49999999999999994d, en JDK 6, se llamará piso y por lo tanto devuelve 1, pero en JDK 7, el if condición es comprobar si el número es el mayor valor doble inferior a 0,5 o no. Como en este caso el número no es el valor doble más grande menor que 0.5, entonces el else bloque regresa 0.

Puede probar 0.49999999999999999d, que devolverá 1, pero no 0, porque este es el mayor valor doble inferior a 0.5.


79
2018-03-28 08:29



Tengo lo mismo en JDK 1.6 de 32 bits, pero en Java 7 de 64 bits tengo 0 para 0,49999999999999994, que redondeado es 0 y la última línea no se imprime. Parece ser un problema de VM, sin embargo, al usar puntos flotantes, debe esperar que los resultados difieran un poco en varios entornos (CPU, modo de 32 o 64 bits).

Y, al usar round o matrices de inversión, etc., estos bits puede hacer una gran diferencia.

salida 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

26
2018-03-28 07:42



La respuesta a continuación es un extracto de un Oráculo  informe de error 6430675 a. Visite el informe para la explicación completa.

Los métodos {Math, StrictMath.round se definen operativamente como

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

para dobles argumentos. Si bien esta definición generalmente funciona como se esperaba, ofrece el sorprendente resultado de 1, en vez de 0, para 0x1.fffffffffffffp-2 (0.49999999999999994).

El valor 0,49999999999999994 es el mayor valor de punto flotante menor que 0,5. Como un literal de coma flotante hexadecimal, su valor es 0x1.fffffffffffffp-2, que es igual a (2 - 2 ^ 52) * 2 ^ -2. == (0.5 - 2 ^ 54). Por lo tanto, el valor exacto de la suma

(0.5 - 2^54) + 0.5

es 1 - 2 ^ 54. Esto está a medio camino entre los dos números de coma flotante adyacentes (1 - 2 ^ 53) y 1. En la ronda aritmética IEEE 754 hasta el modo de redondeo más cercano utilizado por Java, cuando los resultados de coma flotante son inexactos, el más cercano de los dos valores de punto flotante representables que corcheten el resultado exacto deben ser devueltos; si ambos valores son igualmente cercanos, se devolverá el último bit cero. En este caso, el valor de retorno correcto del agregado es 1, no el valor más grande menor que 1.

Mientras que el método está funcionando como se define, el comportamiento en esta entrada es muy sorprendente; la especificación podría enmendarse a algo más parecido a "Redondear al más cercano, redondeando las ataduras hacia arriba", lo que permitiría cambiar el comportamiento en esta entrada.


11
2018-06-05 11:19