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

567

En el siguiente programa, puede ver que cada valor es ligeramente inferior al .5redondeado 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 la actualización de Java 6 31.

Peter Lawrey
fuente
1
En java 1.7.0 funciona bien i.imgur.com/hZeqx.png
Café
2
@Adel: Vea mi comentario sobre la respuesta de Oli , parece que Java 6 implementa esto (y documenta que lo hace ) de una manera que puede causar una mayor pérdida de precisión al agregar 0.5al número y luego usar floor; Java 7 ya no lo documenta de esa manera (presumiblemente / con suerte porque lo arreglaron).
TJ Crowder
1
Fue un error en un programa de prueba que escribí. ;)
Peter Lawrey
1
Otro ejemplo que muestra valores de coma flotante no se puede tomar al pie de la letra.
Michaël Roy
1
Después de pensarlo. No veo un problema 0.49999999999999994 es mayor que el número representable más pequeño menor que 0.5, y la representación en forma decimal legible por humanos es en sí misma una aproximación que está tratando de engañarnos.
Michaël Roy

Respuestas:

574

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 rota. 3 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 menor que 0.5, por lo que cuando se agregan, su mantisa se desplaza 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

Oliver Charlesworth
fuente
No veo esa definición de rounden el Javadoc paraMath.round o en la descripción general de la Mathclase.
TJ Crowder
3
@ Oli: Oh, eso es interesante, sacaron ese bit para Java 7 (los documentos a los que me vinculé), tal vez para evitar causar este tipo de comportamiento extraño al desencadenar una (más) pérdida de precisión.
TJ Crowder
@TJCrowder: Sí, es interesante. ¿Sabes si hay algún tipo de documento de "notas de lanzamiento" / "mejoras" para versiones Java individuales, de modo que podamos verificar esta suposición?
Oliver Charlesworth
66
@MohammadFadin: Eche un vistazo, por ejemplo, en.wikipedia.org/wiki/Single_precision y en.wikipedia.org/wiki/Unit_in_the_last_place .
Oliver Charlesworth
1
No puedo evitar pensar que esta solución es solo cosmética, ya que cero es más visible. No hay duda de que hay muchos otros valores de coma flotante afectados por este error de redondeo.
Michaël Roy
83

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, llamará a piso y, por lo tanto, devuelve 1, pero en JDK 7, la ifcondición 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, entonces elelse bloque devuelve 0.

Puede probar 0.49999999999999999d, que devolverá 1, pero no 0, porque este es el valor doble más grande menor que 0.5.

Chandra Sekhar
fuente
¿Qué pasa con 1.499999999999999994 aquí entonces? devuelve 2? debería devolver 1, pero esto debería obtener el mismo error que antes, pero con un 1.?
mmm
66
1.499999999999999994 no se puede representar en coma flotante de precisión doble. 1.4999999999999998 es el doble más pequeño menor que 1.5. Como puede ver en la pregunta, el floormétodo lo redondea correctamente.
OrangeDog
26

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 roundo invertir matrices, etc., estos bits pueden 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
Marinero danubiano
fuente
En Java 7 (la versión que está utilizando para probarlo) el error está solucionado.
Iván Pérez
1
Creo que quisiste decir 32 bits. Dudo que en.wikipedia.org/wiki/ZEBRA_%28computer%29 pueda ejecutar Java y dudo que haya habido una máquina de 33 bits desde entonces.
chx
@chx, obviamente, porque he escrito 32 bits antes :)
Danubian Sailor
11

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

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

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

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

El valor 0.49999999999999994 es el mayor valor de coma flotante menor que 0.5. Como literal hexadecimal de coma flotante, 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 adyacentes de punto flotante (1 - 2 ^ 53) y 1. En el redondeo aritmético IEEE 754 al modo de redondeo par más cercano utilizado por Java, cuando un resultado de punto flotante es inexacto, el más cercano de los dos los valores representables de coma flotante que entre paréntesis deben devolver el resultado exacto; si ambos valores son igualmente cercanos, se devuelve el ú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 se define, el comportamiento en esta entrada es muy sorprendente; la especificación podría enmendarse a algo más como "Redondear al más largo, el redondeo se vincula", lo que permitiría cambiar el comportamiento de esta entrada.

shiv.mymail
fuente