Pourquoi Math.round (0.4999999999999999994) renvoie 1?

567

Dans le programme suivant, vous pouvez voir que chaque valeur légèrement inférieure à .5est arrondie à l'exception 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);
}

impressions

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

J'utilise Java 6 mise à jour 31.

Peter Lawrey
la source
1
Sur java 1.7.0 cela fonctionne ok i.imgur.com/hZeqx.png
Coffee
2
@Adel: Voir mon commentaire sur la réponse d'Oli , on dirait que Java 6 implémente cela (et documente ce qu'il fait ) d'une manière qui peut entraîner une perte de précision supplémentaire en ajoutant 0.5au nombre puis en utilisant floor; Java 7 ne le documente plus de cette façon (probablement / espérons-le parce qu'ils l'ont corrigé).
TJ Crowder
1
C'était un bug dans un programme de test que j'ai écrit. ;)
Peter Lawrey
1
Un autre exemple qui montre des valeurs à virgule flottante ne peut pas être pris à la valeur nominale.
Michaël Roy
1
Après y avoir réfléchi. Je ne vois aucun problème. 0,49999999999999994 est plus grand que le plus petit nombre représentable inférieur à 0,5, et la représentation sous forme décimale lisible par l'homme est elle-même une approximation qui tente de nous tromper.
Michaël Roy

Réponses:

574

Sommaire

En Java 6 (et probablement plus tôt), round(x)est implémenté en tant que floor(x+0.5). 1 Il s'agit d'un bug de spécification, précisément pour ce cas pathologique. 2 Java 7 ne rend plus obligatoire cette implémentation défectueuse. 3

Le problème

0,5 + 0,49999999999999994 est exactement 1 en double précision:

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
}

C'est parce que 0.4999999999999999994 a un exposant plus petit que 0.5, donc quand ils sont ajoutés, sa mantisse est décalée et l'ULP devient plus grand.

La solution

Depuis Java 7, OpenJDK (par exemple) l'implémente ainsi: 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édits à @SimonNickerson pour l'avoir trouvé)

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
la source
Je ne vois pas cette définition de rounddans le Javadoc pourMath.round ou dans l'aperçu de la Mathclasse.
TJ Crowder
3
@ Oli: Oh, maintenant c'est intéressant, ils ont supprimé ce bit pour Java 7 (les documents auxquels je suis lié) - peut-être afin d'éviter de provoquer ce genre de comportement étrange en déclenchant une (nouvelle) perte de précision.
TJ Crowder
@TJCrowder: Oui, c'est intéressant. Savez-vous s'il existe une sorte de document "notes de publication" / "améliorations" pour les différentes versions de Java, afin que nous puissions vérifier cette hypothèse?
Oliver Charlesworth
6
@MohammadFadin: Jetez un œil par exemple à en.wikipedia.org/wiki/Single_precision et en.wikipedia.org/wiki/Unit_in_the_last_place .
Oliver Charlesworth
1
Je ne peux pas m'empêcher de penser que cette correction n'est que cosmétique, car zéro est le plus visible. Il y a sans aucun doute de nombreuses autres valeurs à virgule flottante affectées par cette erreur d'arrondi.
Michaël Roy
83

Code source dans JDK 6:

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

Code source dans 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;
    }
}

Lorsque la valeur est 0.4999999999999999994d, dans JDK 6, il appellera floor et renvoie donc 1, mais dans JDK 7, la ifcondition vérifie si le nombre est la plus grande valeur double inférieure à 0,5 ou non. Comme dans ce cas, le nombre n'est pas la plus grande valeur double inférieure à 0,5, le elsebloc renvoie donc 0.

Vous pouvez essayer 0.4999999999999999999d, qui renverra 1, mais pas 0, car il s'agit de la plus grande valeur double inférieure à 0,5.

Chandra Sekhar
la source
que se passe-t-il alors avec 1.499999999999999994? renvoie 2? il devrait retourner 1, mais cela devrait vous donner la même erreur que précédemment, mais avec un 1.?
mmm
6
1.499999999999999994 ne peut pas être représenté en virgule flottante double précision. 1,4999999999999998 est le plus petit double inférieur à 1,5. Comme vous pouvez le voir dans la question, la floorméthode l'arrondit correctement.
OrangeDog
26

J'ai le même sur JDK 1.6 32 bits, mais sur Java 7 64 bits, j'ai 0 pour 0.4999999999999999994 qui est arrondi à 0 et la dernière ligne n'est pas imprimée. Il semble que ce soit un problème de machine virtuelle, cependant, en utilisant des virgules flottantes, vous devez vous attendre à ce que les résultats diffèrent un peu selon les environnements (CPU, mode 32 ou 64 bits).

Et, lorsque vous utilisez roundou inversez des matrices, etc., ces bits peuvent faire une énorme différence.

Sortie 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
Marin danubien
la source
Dans Java 7 (la version que vous utilisez pour le tester), le bug est corrigé.
Iván Pérez
1
Je pense que vous vouliez dire 32 bits. Je doute que en.wikipedia.org/wiki/ZEBRA_%28computer%29 puisse exécuter Java et je doute qu'il y ait eu une machine 33 bits depuis.
chx
@chx bien évidemment, parce que j'ai déjà écrit 32 bits :)
Danubian Sailor
11

La réponse ci- dessous est un extrait d'un rapport de bogue Oracle 6430675 sur. Consultez le rapport pour l'explication complète.

Les méthodes {Math, StrictMath.round sont définies opérationnellement comme

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

pour les arguments doubles. Bien que cette définition fonctionne généralement comme prévu, elle donne le résultat surprenant de 1, plutôt que de 0, pour 0x1.fffffffffffffp-2 (0.4999999999999999994).

La valeur 0,49999999999999994 est la plus grande valeur en virgule flottante inférieure à 0,5. En tant que littéral hexadécimal à virgule flottante, sa valeur est 0x1.fffffffffffffp-2, qui est égale à (2 - 2 ^ 52) * 2 ^ -2. == (0,5 - 2 ^ 54). Par conséquent, la valeur exacte de la somme

(0.5 - 2^54) + 0.5

est 1 - 2 ^ 54. Ceci est à mi-chemin entre les deux nombres à virgule flottante adjacents (1 - 2 ^ 53) et 1. Dans le arrondi arithmétique IEEE 754 au mode d'arrondi pair le plus proche utilisé par Java, lorsqu'un résultat à virgule flottante est inexact, le plus proche des deux des valeurs représentatives en virgule flottante qui encadrent le résultat exact doivent être renvoyées; si les deux valeurs sont également proches, celle dont son dernier bit zéro est renvoyé. Dans ce cas, la valeur de retour correcte de l'addition est 1, pas la plus grande valeur inférieure à 1.

Alors que la méthode fonctionne comme définie, le comportement sur cette entrée est très surprenant; la spécification pourrait être modifiée en quelque chose de plus comme «Arrondir à la longueur la plus proche, arrondir les liens», ce qui permettrait de modifier le comportement de cette entrée.

shiv.mymail
la source