web-dev-qa-db-fra.com

PHP - Précision des nombres flottants

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

Résultats dans 0.009999999999998

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

Pourquoi PHP ne renvoie-t-il pas le 0,01 attendu?

76
dcmoody

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 flotteurs a et b, (a+b)-b != a. Cela s'applique à toute langue utilisant des flottants.

Puisque virgule flottante sont des nombres binaires avec une précision finie, il y a une quantité finie de nombres représentables , ce qui entraîne problèmes de précision et surprend comme ça. Voici une autre lecture intéressante: Ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante .


Revenons à votre problème, il n'y a fondamentalement aucun moyen de représenter avec précision 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. Utilisez round($result, 2) sur le résultat pour l'arrondir à 2 décimales.

  2. Utilisez des entiers. Si c'est la devise, par exemple le dollar américain, stockez 35,00 $ en 3500 et 34,99 $ en 3499, puis divisez le résultat par 100.

C'est dommage que PHP n'a pas de type de données décimal comme autrelangues faire.

118
NullUserException

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. C'est tout pour l'ordinateur. La différence entre virgule flottante et entier réside dans la façon dont nous interprétons les 0 et les 1 lorsque nous voulons les regarder.

Un bit est le "signe" (0 = positif, 1 = négatif), 8 bits sont l'exposant (allant de -128 à +127), 23 bits sont le nombre connu comme la "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 "sa place" avec la fraction. Par exemple:

4,39 x 10 ^ 2 = 439

En binaire, "sa place" est un seul bit. Puisque nous ignorons tous les 0 les plus à gauche dans la notation scientifique (nous ignorons tous les chiffres insignifiants), le premier bit est garanti à 1

1.101 x 2 ^ 3 = 1101 = 13

Étant donné que nous sommes garantis que le premier bit sera un 1, nous supprimons ce bit lors du stockage du nombre pour économiser de l'espace. Ainsi, le nombre ci-dessus est stocké en tant que 101 (pour la mantisse). Le 1 de tête est supposé

Par 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

Appliquer 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).

Cependant, pour de nombreux nombres, il n'y a pas de représentation binaire exacte. Tout comme la façon dont 1/3 = 0,333 .... se répète pour toujours, 1/100 est 0,00000010100011110101110000 ..... avec un "10100011110101110000" répétitif. Cependant, un ordinateur 32 bits ne peut pas stocker le nombre entier en virgule flottante. Il fait donc sa meilleure supposition.

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 de 2)

Il doit être immédiatement clair que 01111100101000111101011100001010 ne ressemble en rien à 0,01

Plus important encore, cependant, cela contient une version tronquée d'une décimale répétitive. La décimale d'origine contenait un "10100011110101110000" répété. Nous avons simplifié cela au 01000111101011100001010

En convertissant ce nombre à virgule flottante en décimal via notre formule, nous obtenons 0,0099999979 (notez que c'est pour un ordinateur 32 bits. Un ordinateur 64 bits aurait beaucoup plus de précision)

Un équivalent décimal

Si cela aide à mieux comprendre le problème, regardons la notation scientifique décimale lorsqu'il s'agit de répéter des décimales.

Supposons que nous ayons 10 "boîtes" pour stocker les chiffres. Par conséquent, si nous voulions stocker un nombre comme 1/16, nous écririons:

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

Ce qui est clairement juste 6.25 e -2, où e est un raccourci pour *10^(. Nous avons alloué 4 cases pour la décimale même si nous n'en avions 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 nombres allant de -9.9999 e -9 à +9.9999 e +9

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

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

Ce nouveau numéro 0.66667 n'est pas exactement égal à 2/3. En fait, c'est parti par 0.000003333.... Si nous devions essayer d'écrire 0.66667 en base 3, nous aurions 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étitive plus grande, comme 1/7. Cela a 6 chiffres répétitifs: 0.142857142857...

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

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

Ce nombre, 0.14286, est désactivé par .000002857...

C'est "proche de corriger", mais ce n'est pas exactement correct, et donc si nous avons essayé d'écrire ce nombre dans base 7, nous obtiendrions un nombre hideux au lieu de 0.1. En fait, en branchant ceci dans Wolfram Alpha, nous obtenons: .10000022320335...

Ces différences fractionnaires mineures devraient vous être familières 0.0099999979 (par opposition à 0.01)

47
stevendesu

Il y a beaucoup de réponses ici sur pourquoi 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 l'extension BC Math (qui n'est en fait qu'une BigNum, Arbitrary Précision implémentation ...

Pour ajouter deux nombres:

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

entraînera 12345678901235.1234567890 ...

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

Vous ne pouvez donc pas faire 1/3 Avec une précision de 100%, car il a une décimale répétitive (et donc n'est pas rationnel).

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

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.

Aussi, autre chose à noter ici. PHP ne peut représenter que des entiers 32 bits ou 64 bits (selon votre installation). Donc, si un entier dépasse la taille du type int natif (2,1 milliards pour 32 bits, 9,2 x10 ^ 18, ou 9,2 milliards de milliards pour les entrées signées), PHP convertira l'int en flottant. Bien que ce ne soit pas immédiatement un problème (puisque toutes les entrées plus petites que la précision du flottant du système sont par définition directement représentables) sous forme de flotteurs), si vous essayez de multiplier deux ensemble, cela perdra une précision importante.

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

Comme un nombre, $n Sera float(40000000002), ce qui est bien car il est exactement représenté. Mais si on le met au carré, on obtient: float(1.60000000016E+21)

En tant que chaîne (en utilisant BC math), $n Sera exactement '40000000002'. Et si on le met au carré, on obtient: string(22) "1600000000160000000004"...

Donc, si vous avez besoin de précision avec de grands nombres ou des décimales rationnelles, vous voudrez peut-être examiner bcmath ...

14
ircmaxell

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

Cette réponse résout le problème, mais n'explique pas pourquoi. Je pensais que c'était évident [je programme aussi en C++, donc c'est IS évident pour moi;]], mais sinon, disons que PHP a sa propre précision de calcul et dans cette situation particulière, il a retourné la plupart des informations conformes concernant ce calcul.

2
Tomasz Kowalczyk

bcadd () pourrait être utile ici.

<?PHP

$a = '35';
$b = '-34.99';

echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);

?>

(sortie inefficace pour plus de clarté)

La première ligne me donne 0,00999999999999998. La seconde me donne 0,01

2
Pickle

Parce que 0,01 ne peut pas être représenté exactement comme la somme d'une série de fractions binaires. Et c'est ainsi que les flotteurs 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 corriger, voir d'autres réponses.

1
Andrey

mon php renvoie 0,01 ... alt text

peut-être qu'il faut faire avec la version php, (j'utilise 5.2)

1

[Résolu]

enter image description here

Chaque numéro sera enregistré dans l'ordinateur par une valeur binaire telle que 0, 1. Dans les nombres simple précision occupent 32 bits.

Le nombre à virgule flottante peut être présenté par: 1 bit pour le signe, 8 bits pour l'exposant et 23 bits appelés mantisse (fraction).

Regardez l'exemple ci-dessous:

0,15625 = 0,00101 = 1,01 * 2 ^ (- 3)

enter image description here

  • signe: 0 nombre positif moyen, 1 nombre négatif moyen, dans ce cas c'est 0.

  • exposant: 01111100 = 127 - 3 = 124.

    Remarque: le biais = 127 exposant donc biaisé = −3 + le "biais". En simple précision, la polarisation est de 127, donc dans cet exemple l'exposant polarisé est de 124;

  • À la partie fraction, nous avons: 1,01 moyenne: 0 * 2 ^ -1 + 1 * 2 ^ -2

    Le numéro 1 (première position de 1,01) n'a pas besoin d'être sauvegardé car lorsqu'il est présent le nombre flottant de cette manière, le premier nombre est toujours 1. Par exemple, convertissez: 0,11 => 1,1 * 2 ^ (- 1), 0,01 => 1 * 2 ^ (- 2).

Un autre exemple montre toujours supprimer le premier zéro: 0,1 sera présenté 1 * 2 ^ (- 1). Donc, la première alwasy est 1. Le nombre actuel de 1 * 2 ^ (- 1) sera:

  • 0: nombre positif
  • 127-1 = 126 = 01111110
  • fraction: 00000000000000000000000 (23 nombre)

Enfin: le binaire brut est: 0 01111110 00000000000000000000000

Vérifiez-le ici: http://www.binaryconvert.com/result_float.html?decimal=04804605

Maintenant, si vous comprenez déjà comment un nombre à virgule flottante est enregistré. Que se passe-t-il si le nombre ne peut pas être enregistré en 32 bits (précision simple).

Par exemple: en décimal. 1/3 = 0,333333333333333333333333 et parce qu'il est infini, je suppose que nous avons 5 bits pour enregistrer les données. Répétez à nouveau ce n'est pas réel. suppose juste. Ainsi, les données enregistrées dans l'ordinateur seront:

0.33333.

Maintenant, lorsque le nombre chargé l'ordinateur calculer à nouveau:

0.33333 = 3*10^-1 + 3*10^-2 + 3*10^-3 + 3*10^-4 +  3*10^-5.

À propos de ça:

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

Le résultat est 0,01 (décimal). Maintenant, montrons ce nombre en binaire.

0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary)

Vérifiez ici: http://www.binaryconvert.com/result_double.html?decimal=048046048049

Parce que (01011100001010001111) se répète comme 1/3. L'ordinateur ne peut donc pas enregistrer ce numéro dans leur mémoire. Il faut sacrifier. Cela ne conduit pas à la précision de l'ordinateur.

Avancé (Vous devez avoir des connaissances en mathématiques) Alors pourquoi nous pouvons facilement afficher 0,01 en décimal mais pas en binaire.

Supposons que la fraction en binaire de 0,01 (décimal) soit finie.

So 0.01 = 2^x + 2^y... 2^-z
0.01 * (2^(x+y+...z)) =  (2^x + 2^y... 2^z)*(2^(x+y+...z)). This expression is true when (2^(x+y+...z)) = 100*x1. There are not integer n = x+y+...+z exists. 

=> So 0.01 (decimal) must be infine in binary.
1
christian Nguyen

ne serait-il pas plus simple d'utiliser number_format(0.009999999999998, 2) ou $res = $a+$b; -> number_format($res, 2);?

0
Jurijs Nesterovs