2012-03-28 5 views
525

Nel seguente programma è possibile vedere che ciascun valore leggermente inferiore a .5 viene arrotondato per difetto, ad eccezione di 0.5.Perché Math.round (0.49999999999999994) restituisce 1?

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

+0

Nel mio caso la risposta è 0 e non 1 e l'ultima riga non viene stampata mentre interrompe il loop lì. Qui è la mia uscita ...... 8,499999999999998 arrotondato è 8 7.5 arrotondato è 8 7,499999999999999 arrotondato è 7 6.5 arrotondato è 7 6,499999999999999 arrotondata è 6 5.5 arrotondata è 6 5,499999999999999 arrotondato è 5 4,5 arrotondata è 5 4.499999999999999 arrotondato è 4 3.5 arrotondato è 4 3.4999999999999996 arrotondato è 3 2,5 arrotondato è 3 2.4999999999999996 sferici sono 2 1,5 arrotondato è 2 1,4999999999999998 arrotondate è 1 0,5 arrotondato è 1 0,49999999999999994 arrotondato è 0 –

+1

su Java 1.7.0 funziona bene http://i.imgur.com/hZeqx.png – Coffee

+2

@Adel: Vedi il mio commento su [risposta di Oli] (http://stackoverflow.com/a/9903075/157247), sembra che Java 6 implementa questo (e [documenti che lo fa] (http://docs.oracle.com/javase /6/docs/api/java/lang/Math.html#round(double))) in un modo che può causare un'ulteriore perdita di precisione aggiungendo '0.5' al numero e quindi usando' floor'; Java 7 [non lo documenta più in questo modo] (http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html#round (double)) (presumibilmente/si spera perché lo hanno risolto). –

risposta

544

Sommario

In Java 6 (e presumibilmente in precedenza), round(x) viene implementato come floor(x+0.5). Questo è un bug di specifica, proprio per questo caso patologico. Java 7 non richiede più questa implementazione interrotta.

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,49999999999999994 ha un esponente minore di 0,5, in modo da quando sono aggiunti, la sua mantissa è spostata e l'ULP si ingrandisce.

La soluzione

Da Java 7, OpenJDK (per esempio) implementa così:

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 la ricerca di questo)

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

+0

Non vedo la definizione di 'round' in [Javadoc per' Math.round'] (http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html# round (double)) o nella panoramica della classe 'Math'. –

+0

@ T.J Crowder: È sicuramente presente nei documenti SE6. –

+3

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

26

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

E, quando si utilizza round o invertendo 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 
+0

In Java 7 (la versione che si sta utilizzando per testarlo) il bug è corretto. –

+1

Penso che intendessi 32 bit. Dubito che http://en.wikipedia.org/wiki/ZEBRA_%28computer%29 possa eseguire Java e dubito che da allora ci sia stata una macchina a 33 bit. – chx

+0

@chx abbastanza ovviamente, perché ho scritto 32 bit prima :) –

228

questo sembra essere un bug (Java bug 6430675: Math.round has surprising behavior for 0x1.fffffffffffffp-2), che è stato fissato in Java 7.

+5

+1: Bella scoperta! Si collega alle differenze di documentazione tra Java 6 e 7 come spiegato nella mia risposta. –

+0

L'implementazione round() è [molto più difficile di quanto la maggior parte potrebbe pensare] (https: // StackOverflow.com/a/24348037/1708801) –

79

codice sorgente in JDK 6:

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

Fonte codice 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.49999999999999994d, in JDK 6, chiamerà floor e quindi restituisce 1, ma in JDK 7, la condizione if sta verificando se il numero è il doppio valore massimo minore di 0,5 o meno. Come in questo caso il numero non è il doppio valore massimo inferiore a 0,5, quindi il blocco else restituisce 0.

Puoi provare a 0,49999999999999999d, che restituirà 1, ma non 0, perché questo è il doppio valore massimo in meno di 0,5.

+0

cosa succede allora con 1.499999999999999994? restituisce 2? dovrebbe restituire 1, ma questo dovrebbe farti lo stesso errore di prima, ma con un 1.? – momomo

+5

1.499999999999999994 non può essere rappresentato in virgola mobile a precisione doppia. 1.4999999999999998 è il doppio più piccolo meno di 1,5. Come puoi vedere dalla domanda, il metodo 'floor' lo arrotonda correttamente. – OrangeDog

+4

La chiave è che i doppi non sono distribuiti in modo uniforme. – OrangeDog

11

La risposta qui di seguito è un estratto di Oraclebug report 6430675 a. Visita il rapporto per la spiegazione completa.

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

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

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

Il valore 0,49999999999999994 è il più grande 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. Nella modalità aritmetica IEEE 754 arrotondata alla modalità di arrotondamento uniforme più vicina usata da Java, quando un risultato a virgola mobile è inesatto, il più vicino dei due i valori in virgola mobile rappresentabili che corrispondono al risultato esatto devono essere restituiti; se entrambi i valori sono ugualmente vicini, viene restituito quello che ha il suo ultimo bit zero. In questo caso il valore di ritorno corretto dall'aggiunta è 1, non il valore massimo 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 al "Round to the long longing, rounding più vicino", che consentirebbe di modificare il comportamento su questo input.