¿Por qué las Matemáticas.ronda (0.499999999999999994) retorno 1?


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

Imprime

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 update 31.

Author: Peter Mortensen, 2012-03-28

5 answers

Resumen

En Java 6 (y presumiblemente anterior), 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 rota.3

El problema

0.5+0.499999999999999994 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.4999999999999994 tiene un exponente más pequeño que 0.5, por lo que cuando se agregan, su mantisa es desplazado, y el ULP se hace más grande.

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

 550
Author: Oliver Charlesworth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-25 19:01:06

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

 228
Author: Simon Nickerson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-28 07:53: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.49999999999999994 d, en JDK 6, llamará a floor y por lo tanto devuelve 1, pero en JDK 7, la condición if es verificar si el número es el mayor valor doble menor que 0.5 o no. Como en este caso el número no es el mayor valor doble menor que 0.5, por lo que el bloque else devuelve 0.

Puede probar 0.499999999999999999 d, que devolverá 1, pero no 0, porque esto es el mayor valor doble menor que 0.5.

 80
Author: Chandra Sekhar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-15 18:41:51

Tengo lo mismo en JDK 1.6 32-bit, pero en Java 7 64-bit 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 invertidas, etc., estos bits pueden hacer una gran diferencia.

X64 salida:

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
Author: Danubian Sailor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-15 18:23:58

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

Los métodos {Math, StrictMath.las rondas se definen operacionalmente como

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

Para argumentos dobles. Si bien esta definición generalmente funciona como se espera, da el sorprendente resultado de 1, en lugar de 0, para 0x1.fffffffffffffp-2 (0.4999999999999994).

El valor 0.49999999999999994 es el mayor valor de coma flotante menos de 0.5. Como 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 al modo de redondeo par más cercano utilizado por Java, cuando un resultado de coma flotante es inexacto, el más cercano de los dos valores de coma flotante representables que encierran el valor exacto se debe devolver el resultado; si ambos valores están igual de cerca, se devuelve el que tiene su último bit cero. En este caso, el valor de retorno correcto de la suma es 1, no el mayor valor menor que 1.

Mientras el método funciona como está definido, el comportamiento de esta entrada es muy sorprendente; la especificación podría modificarse a algo más como "Redondear a lo más cercano, redondear lazos", lo que permitiría cambiar el comportamiento de esta entrada.

 11
Author: shiv.mymail,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-15 19:31:54