Perché Math.round (0.49999999999999994) restituisce 1?


567

Nel seguente programma puoi vedere che ogni valore è leggermente inferiore a quello .5arrotondato per difetto, ad eccezione di 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);
}

stampe

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

Sto usando Java 6 aggiornamento 31.


1
Su java 1.7.0 funziona bene i.imgur.com/hZeqx.png
Caffè

2
@Adel: vedi il mio commento sulla risposta di Oli , sembra che Java 6 lo implementa (e documenta ciò ) in un modo che può causare un'ulteriore perdita di precisione aggiungendo 0.5al numero e quindi usando floor; Java 7 non lo documenta più in questo modo (presumibilmente / si spera perché lo hanno risolto).
TJ Crowder,

1
Era un bug in un programma di test che ho scritto. ;)
Peter Lawrey,

1
Un altro esempio che mostra i valori in virgola mobile non può essere preso al valore nominale.
Michaël Roy,

1
Dopo averci pensato. Non vedo un problema. 0.49999999999999994 è maggiore del numero rappresentabile più piccolo inferiore a 0,5 e la rappresentazione in forma decimale leggibile dall'uomo è essa stessa un'approssimazione che sta cercando di ingannarci.
Michaël Roy,

Risposte:


574

Sommario

In Java 6 (e presumibilmente prima), round(x)è implementato come floor(x+0.5). 1 Questo è un bug di specifica, proprio per questo caso patologico. 2 Java 7 non impone più questa implementazione rotta. 3

Il problema

0,5 + 0,49999999999999994 è esattamente 1 in doppia precisione:

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
}

Questo perché 0.4999999999999999994 ha un esponente più piccolo di 0,5, quindi quando vengono aggiunti, la sua mantissa viene spostata e l'ULP diventa più grande.

La soluzione

Dal momento che Java 7, OpenJDK (per esempio) lo implementa così: 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 (crediti a @SimonNickerson per averlo trovato)

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


Non vedo quella definizione roundnel Javadoc perMath.round o nella panoramica della Mathclasse.
TJ Crowder,

3
@ Oli: Oh, ora è interessante, hanno preso quel bit per Java 7 (i documenti a cui mi sono collegato) - forse per evitare di causare questo tipo di comportamento strano innescando una (ulteriore) perdita di precisione.
TJ Crowder,

@TJCrowder: Sì, è interessante. Sai se esiste qualche tipo di documento "note sulla versione" / "miglioramenti" per le singole versioni di Java, in modo da poter verificare questo assunto?
Oliver Charlesworth


1
Non posso fare a meno di pensare che questa correzione sia solo estetica, poiché lo zero è più visibile. Non vi sono dubbi su molti altri valori in virgola mobile interessati da questo errore di arrotondamento.
Michaël Roy,


83

Codice sorgente in JDK 6:

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

Codice sorgente in 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 il valore è 0.4999999999999999994d, in JDK 6, chiamerà floor e quindi restituisce 1, ma in JDK 7, ilif condizione sta verificando se il numero è il doppio valore massimo inferiore a 0,5 o meno. Come in questo caso il numero non è il doppio valore massimo inferiore a 0,5, quindi il elseblocco restituisce 0.

Puoi provare 0.4999999999999999999d, che restituirà 1, ma non 0, poiché questo è il doppio valore massimo inferiore a 0,5.


cosa succede con 1.49999999999999999994 qui allora? restituisce 2? dovrebbe restituire 1, ma questo dovrebbe ottenere lo stesso errore di prima, ma con un 1.?
mmm

6
1.499999999999999994 non può essere rappresentato in virgola mobile a precisione doppia. 1.4999999999999998 è il doppio più piccolo inferiore a 1,5. Come puoi vedere dalla domanda, il floormetodo la arrotonda correttamente.
OrangeDog

26

Ho lo stesso su JDK 1.6 a 32 bit, ma su Java 7 a 64 bit ho 0 per 0,4999999999999999994 che arrotondato è 0 e l'ultima riga non viene stampata. Sembra essere un problema di VM, tuttavia, usando i punti mobili, dovresti aspettarti che i risultati differiscano un po 'su vari ambienti (CPU, modalità 32 o 64 bit).

E, quando si usano roundo si invertono matrici, ecc., Questi bit possono fare un'enorme differenza.

uscita 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

In Java 7 (la versione che si sta utilizzando per testarlo) il bug è stato corretto.
Iván Pérez,

1
Penso che volevi dire a 32 bit. Dubito che en.wikipedia.org/wiki/ZEBRA_%28computer%29 potrebbe eseguire Java e dubito che da allora sia esistita una macchina a 33 bit.
chx,

@chx ovviamente, perché ho scritto 32 bit prima :)
Danubian Sailor,

11

La risposta di seguito è un estratto di un bug report Oracle 6430675 all'indirizzo. Visita il rapporto per la spiegazione completa.

I metodi {Math, StrictMath.round sono definiti operativamente come

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

per doppi argomenti. Mentre questa definizione di solito funziona come previsto, fornisce il risultato sorprendente di 1, anziché 0, per 0x1.fffffffffffffp-2 (0.49999999999999994).

Il valore 0.49999999999999994 è il massimo valore in virgola mobile inferiore a 0,5. Come valore letterale in virgola mobile esadecimale, il suo valore è 0x1.fffffffffffffp-2, che è uguale a (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Pertanto, il valore esatto della somma

(0.5 - 2^54) + 0.5

è 1 - 2 ^ 54. Questo è a metà strada tra i due numeri in virgola mobile adiacenti (1 - 2 ^ 53) e 1. Nell'IEEE 754 arrotondamento aritmetico alla modalità di arrotondamento uniforme più vicina utilizzata da Java, quando i risultati in virgola mobile sono inesatti, il più vicino dei due devono essere restituiti valori in virgola mobile rappresentabili che racchiudono il risultato esatto; se entrambi i valori sono ugualmente vicini, quello a cui viene restituito il suo ultimo bit zero. In questo caso il valore di ritorno corretto dall'aggiunta è 1, non il valore più grande inferiore a 1.

Mentre il metodo funziona come definito, il comportamento su questo input è molto sorprendente; la specifica potrebbe essere modificata in qualcosa di più simile a "Arrotonda al più vicino lungo, legami di arrotondamento", che consentirebbe di modificare il comportamento su questo input.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.