93 votes

PHP - Précision des nombres flottants

$a = '35';
$b = '-34.99';
echo ($a + $b);

Résultat : 0,009999999999998

Qu'est-ce qui se passe avec ça ? Je me demandais pourquoi mon programme continuait à rapporter des résultats bizarres.

Pourquoi PHP ne renvoie pas le résultat attendu de 0.01 ?

16 votes

Je vous suggère de lire nombres à virgule flottante . Plus précisément, les sections "Nombres représentables, conversion et arrondis" y "Problèmes de précision" . Le reste de l'article est bon si vous voulez comprendre comment ils fonctionnent, mais ces deux sections s'appliquent spécifiquement à votre question...

9 votes

Il est également intéressant de noter que vous utilisez des chaînes de caractères au lieu de nombres (ils sont implicitement convertis, mais quand même). Faites $a = 35; $b = -34.99 à la place.

0 votes

133voto

NullUserException Points 42268

Parce que l'arithmétique en virgule flottante != l'arithmétique des nombres réels. Une illustration de la différence due à l'imprécision est, pour certains flottants a y b , (a+b)-b != a . Ceci s'applique à tout langage utilisant des flottants.

Desde virgule flottante sont des nombres binaires avec une précision finie, il y a une quantité finie de nombres représentables ce qui conduit problèmes de précision et des surprises comme celle-ci. Voici une autre lecture intéressante : Ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante .


Pour en revenir à votre problème, il n'y a aucun moyen de représenter précisément 34,99 ou 0,01 en binaire (tout comme en décimal, 1/3 = 0,3333...), donc des approximations sont utilisées à la place. Pour contourner le problème, vous pouvez :

  1. Utilice round($result, 2) sur le résultat pour l'arrondir à 2 décimales.

  2. Utilisez des nombres entiers. S'il s'agit d'une devise, disons des dollars américains, enregistrez 35,00 $ sous la forme de 3500 et 34,99 $ sous la forme de 3499, puis divisez le résultat par 100.

Il est dommage que PHP ne dispose pas d'un type de données décimal tel que autre langues faire.

0 votes

J'ajouterais que 0.01 ne peut pas non plus être représenté tel quel. cela devrait être marqué comme correct, car cela donne une explication et comment corriger. mais pour augmenter l'utilité de cela, veuillez expliquer un peu pourquoi fp != real, avec tous ces trucs binaires et la perte de précision.

0 votes

@irc Merci. J'ai incorporé des parties de votre commentaire dans la réponse

8 votes

Une remarque pédante : il existe un ensemble fini de flottants a y b donde (a+b)-b == a . Il suffit qu'ils aient à la fois un facteur premier de 2 et qu'ils soient représentables dans le nombre approprié de bits (environ 7 chiffres décimaux pour la simple précision, 16 pour la double). Ainsi, a = 0.5 y b = 0.25 fonctionne (et fonctionnera toujours pour les systèmes avec des flottants 32 bits à précision unique). Pour les flottants qui ne remplissent pas l'une ou l'autre ou les deux conditions préalables, alors (a+b)-b != a . Mais si les deux a y b répondent à ces conditions préalables, alors (a+b)-b == a devrait être vrai (mais c'est un ensemble fini)...

62voto

stevendesu Points 2815

Les nombres à virgule flottante, comme tous les nombres, doivent être stockés en mémoire sous la forme d'une chaîne de 0 et de 1. Ce sont tous des bits pour l'ordinateur. La différence entre les nombres à virgule flottante et les nombres entiers réside dans la manière dont nous interprétons les 0 et les 1 lorsque nous voulons les consulter.

Un bit est le "signe" (0 = positif, 1 = négatif), 8 bits sont l'exposant (allant de -128 à +127), 23 bits sont le nombre appelé "mantisse" (fraction). Ainsi, la représentation binaire de (S1)(P8)(M23) a la valeur (-1^S)M*2^P

La "mantisse" prend une forme particulière. En notation scientifique normale, nous affichons la "place de l'un" avec la fraction. Par exemple :

4.39 x 10^2 = 439

En binaire, la "place du un" est un seul bit. Puisque nous ignorons tous les 0 les plus à gauche dans la notation scientifique (nous ignorons tous les chiffres non significatifs), le premier bit est garanti comme étant un 1.

1.101 x 2^3 = 1101 = 13

Puisque nous avons la garantie que le premier bit sera un 1, nous supprimons ce bit lors du stockage du nombre pour gagner de la place. Le nombre ci-dessus est donc stocké sous la forme 101 (pour la mantisse). Le 1 de tête est supposé

À titre d'exemple, prenons la chaîne binaire

00000010010110000000000000000000

Le décomposer en ses composants :

Sign    Power           Mantissa
 0     00000100   10110000000000000000000
 +        +4             1.1011
 +        +4       1 + .5 + .125 + .0625
 +        +4             1.6875

En appliquant notre formule simple :

(-1^S)M*2^P
(-1^0)(1.6875)*2^(+4)
(1)(1.6875)*(16)
27

En d'autres termes, 00000010010110000000000000000000 est 27 en virgule flottante (selon les normes IEEE-754).

Pour de nombreux nombres, il n'existe cependant pas de représentation binaire exacte. De la même manière que 1/3 = 0,333.... se répétant indéfiniment, 1/100 est 0,000000101000111101110000..... avec un "10100011110101110000" répété. Un ordinateur 32 bits ne peut cependant pas stocker le nombre entier en virgule flottante. Il fait donc sa meilleure estimation.

0.0000001010001111010111000010100011110101110000

Sign    Power           Mantissa
 +        -7     1.01000111101011100001010
 0    -00000111   01000111101011100001010
 0     11111001   01000111101011100001010
01111100101000111101011100001010

(notez que le 7 négatif est produit en utilisant le complément à 2)

Il devrait être immédiatement clair que 01111100101000111101011100001010 ne ressemble en rien à 0.01

Mais surtout, il contient une version tronquée d'une décimale répétitive. La décimale originale contenait une répétition de "10100011110101110000". Nous l'avons simplifié en 01000111101011100001010.

En retraduisant ce nombre à virgule flottante en décimal à l'aide de notre formule, nous obtenons 0,0099999979 (notez que ceci est pour un ordinateur 32 bits. Un ordinateur 64 bits aurait une précision bien supérieure)

Un équivalent décimal

Si cela permet de mieux comprendre le problème, examinons la notation scientifique décimale lorsqu'il s'agit de décimales répétées.

Supposons que nous ayons 10 "cases" pour stocker les chiffres. Par conséquent, si nous voulons stocker un nombre comme 1/16, nous devons écrire :

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+

Ce qui est clairement juste 6.25 e -2 , donde e est un raccourci pour *10^( . Nous avons alloué 4 cases pour la décimale alors que nous n'en avons besoin que de 2 (remplissage avec des zéros), et nous avons alloué 2 cases pour les signes (une pour le signe du nombre, une pour le signe de l'exposant).

En utilisant 10 cases comme celle-ci, nous pouvons afficher des chiffres allant de -9.9999 e -9 a +9.9999 e +9

Cela fonctionne bien pour tout ce qui comporte 4 décimales ou moins, mais que se passe-t-il lorsque nous essayons de stocker un nombre tel que 2/3 ?

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

Ce nouveau numéro 0.66667 n'est pas exactement égal à 2/3 . En fait, il est décalé de 0.000003333... . Si nous devions essayer d'écrire 0.66667 en base 3, on obtiendrait 0.2000000000012... au lieu de 0.2

Ce problème peut devenir plus apparent si nous prenons quelque chose avec une décimale répétée plus grande, comme 1/7 . Celui-ci comporte 6 chiffres répétitifs : 0.142857142857...

En stockant ceci dans notre ordinateur décimal, nous ne pouvons montrer que 5 de ces chiffres :

+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

Ce numéro, 0.14286 , s'éteint par .000002857...

C'est "presque correct", mais ce n'est pas exactement correct et donc, si nous essayions d'écrire ce nombre en base 7, nous obtiendrions un nombre hideux au lieu de 0.1 . En fait, en entrant dans Wolfram Alpha, on obtient : .10000022320335...

Ces petites différences fractionnaires devraient vous sembler familières. 0.0099999979 (par opposition à 0.01 )

1 votes

+1 merci, maintenant je sais comment les flottants sont stockés. PS : Non, Windows ne le fait pas. Au moins en PHP5.3.1/Win7 je a fait ont des problèmes de virgule flottante ;)

1 votes

Le dernier paragraphe (qui affirme que le système d'exploitation décide d'arrondir ou non les valeurs à virgule flottante) doit être supprimé. Le résultat d'un calcul en virgule flottante est imposé par la norme IEEE 754, donc "0,1 + 0,2 == 0,3". doit évalue à false sur tout système conforme. Certains programmes dépendent des opérations en virgule flottante qui se comportent de cette manière.

1 votes

@AdamP.Goucher J'ai mis à jour mon post le 15 février selon votre commentaire. J'ai négligé de faire un commentaire ici pour le mentionner, alors je le fais maintenant. Merci pour l'amélioration de la réponse.

18voto

ircmaxell Points 74865

Il y a beaucoup de réponses ici sur la raison pour laquelle les nombres à virgule flottante fonctionnent comme ils le font...

Mais il est peu question de précision arbitraire (Pickle l'a mentionné). Si vous voulez (ou avez besoin) d'une précision exacte, la seule façon de le faire (pour les nombres rationnels au moins) est d'utiliser la fonction BC Math (qui n'est en fait qu'une BigNum, précision arbitraire la mise en œuvre...

Pour additionner deux nombres :

$number = '12345678901234.1234567890';
$number2 = '1';
echo bcadd($number, $number2);

aura pour résultat 12345678901235.1234567890 ...

C'est ce qu'on appelle les mathématiques à précision arbitraire. En fait, tous les nombres sont des chaînes de caractères qui sont analysées pour chaque opération et les opérations sont effectuées chiffre par chiffre (pensez à la division longue, mais effectuée par la bibliothèque). Cela signifie donc que c'est assez lent (par rapport aux constructions mathématiques ordinaires). Mais il est très puissant. Vous pouvez multiplier, ajouter, soustraire, diviser, trouver le modulo et exponentiser n'importe quel nombre qui a une représentation exacte de la chaîne.

Donc vous ne pouvez pas faire 1/3 avec une précision de 100%, puisqu'il a une décimale répétée (et n'est donc pas rationnel).

Mais, si vous voulez savoir ce que 1500.0015 au carré est :

L'utilisation de flottants 32 bits (double précision) donne le résultat estimé de :

2250004.5000023

Mais bcmath donne la réponse exacte de :

2250004.50000225

Tout dépend de la précision dont vous avez besoin.

De plus, il y a autre chose à noter ici. PHP ne peut représenter que des entiers 32 bits ou 64 bits (en fonction de votre installation). Donc si un entier dépasse la taille du type int natif (2.1 milliards pour le 32 bits, 9.2 x10^18, ou 9.2 milliards de milliards pour les ints signés), PHP convertira l'int en un float. Bien que ce ne soit pas un problème immédiat (puisque tous les entiers plus petits que la précision des flottants du système sont par définition directement représentables comme des flottants), si vous essayez de multiplier deux entiers ensemble, vous perdrez une précision significative.

Par exemple, étant donné $n = '40000000002' :

Comme un numéro, $n sera float(40000000002) ce qui est bien puisque c'est exactement représenté. Mais si on le met au carré, on obtient : float(1.60000000016E+21)

Comme une chaîne (en utilisant les mathématiques de la CB), $n sera exactement '40000000002' . Et si on le met au carré, on obtient : string(22) "1600000000160000000004" ...

Donc, si vous avez besoin de la précision avec de grands nombres, ou de points décimaux rationnels, vous devriez peut-être vous tourner vers bcmath...

4 votes

Nitpick : un nombre, tel que 1/3, peut avoir une représentation décimale répétitive et être quand même rationnel. Les "nombres rationnels" sont tous les nombres qui peuvent être présentés comme une fraction de deux nombres a et b où a et b sont tous deux des entiers. Et 1/3 est effectivement un exemple d'un tel nombre.

1 votes

+1 je suis venu ici pour chercher une méthode pour diviser une énorme chaîne par une autre et j'ai trouvé bcmath dans votre réponse. Merci !

0 votes

Je pense que c'est un peu fermé d'esprit de dire que le sólo façon de le faire est d'utiliser bc_math. Je dirais que la façon recommandée de le faire est d'utiliser bc_math. Vous êtes libre d'implémenter votre propre système si vous le souhaitez :D Mais c'est beaucoup plus compliqué que cela n'en vaut la peine.

3voto

Andrey Points 36869

Parce que 0,01 ne peut pas être représenté exactement comme une somme de séries de fractions binaires. Et c'est ainsi que les flottants sont stockés en mémoire.

Je suppose que ce n'est pas ce que vous voulez entendre, mais c'est la réponse à la question. Pour savoir comment réparer, consultez les autres réponses.

0 votes

Somme de séries de binaires quoi ? Ce n'est pas comme ça que les flottants sont stockés. Un flottant est essentiellement une notation scientifique en binaire. Un bit est le "signe" (0 = positif, 1 = négatif), 8 bits sont l'exposant (allant de -128 à +127), 23 bits sont le nombre appelé "mantisse". Ainsi, la représentation binaire de (S1)(P8)(M23) a pour valeur (-1^S)M*2^P

0 votes

@steven_desu merci pour la leçon. l'élément clé ici est que la mantisse est stockée comme une fraction binaire. c'est la réponse à la question "pourquoi" les fractions décimales ne peuvent pas être stockées précisément.

0voto

Tomasz Kowalczyk Points 7520

Utilisez la fonction round() fonction : http://php.net/manual/en/function.round.php

Cette réponse résout le problème, mais n'explique pas pourquoi. Je pense que c'est évident [je programme aussi en C++, donc c'est évident pour moi ;]], mais si ce n'est pas le cas, disons que PHP a sa propre précision de calcul et que dans cette situation particulière, il renvoie les informations les plus conformes concernant ce calcul.

5 votes

-1 Parce que ce n'est absolument pas une réponse à la question.

0 votes

@Dennis Haarbrink eh bien, vous avez rétrogradé cette réponse, quelqu'un a rétrogradé ma réponse. eh bien, et alors ? es réponse alors ?

0 votes

@Andrey : Ouais, je ne sais pas pourquoi ta réponse a été rétrogradée puisque c'est à peu près la bonne réponse :) La meilleure réponse, à mon avis, est celle de @ircmaxell dans les commentaires de l'OP.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X