Considérez le code suivant:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Pourquoi ces inexactitudes se produisent-elles?
Considérez le code suivant:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Pourquoi ces inexactitudes se produisent-elles?
Réponses:
Les mathématiques en virgule flottante binaire sont comme ça. Dans la plupart des langages de programmation, il est basé sur la norme IEEE 754 . Le nœud du problème est que les nombres sont représentés dans ce format comme un nombre entier fois une puissance de deux; les nombres rationnels (comme 0.1
, qui est 1/10
) dont le dénominateur n'est pas une puissance de deux ne peuvent pas être représentés exactement.
Pour 0.1
dans le binary64
format standard , la représentation peut être écrite exactement comme
0.1000000000000000055511151231257827021181583404541015625
en décimal, ou0x1.999999999999ap-4
en notation C99 hexfloat .En revanche, le nombre rationnel 0.1
, qui est 1/10
, peut être écrit exactement comme
0.1
en décimal, ou0x1.99999999999999...p-4
dans un analogue de la notation hexfloat C99, où le ...
représente une séquence sans fin de 9.Les constantes 0.2
et 0.3
dans votre programme seront également des approximations de leurs vraies valeurs. Il arrive que le plus proche double
de 0.2
soit plus grand que le nombre rationnel 0.2
mais que le plus proche double
de 0.3
soit plus petit que le nombre rationnel 0.3
. La somme de 0.1
et 0.2
finit par être supérieure au nombre rationnel 0.3
et donc en désaccord avec la constante de votre code.
Un traitement assez complet des problèmes d'arithmétique en virgule flottante est ce que tout informaticien devrait savoir sur l' arithmétique en virgule flottante . Pour une explication plus facile à digérer, voir flottant-point-gui.de .
Note latérale: Tous les systèmes numériques positionnels (base-N) partagent ce problème avec précision
Les anciens nombres décimaux (base 10) ont les mêmes problèmes, c'est pourquoi des nombres comme 1/3 finissent par 0,333333333 ...
Vous venez de tomber sur un nombre (3/10) qui se révèle facile à représenter avec le système décimal, mais ne correspond pas au système binaire. Cela va dans les deux sens (dans une certaine mesure): 1/16 est un nombre laid en décimal (0,0625), mais en binaire, il semble aussi net qu'un 10 000e en décimal (0,0001) ** - si nous étions en l'habitude d'utiliser un système de nombres de base 2 dans notre vie quotidienne, vous regarderiez même ce nombre et comprendriez instinctivement que vous pourriez y arriver en réduisant de moitié quelque chose, en le réduisant encore et encore, encore et encore.
** Bien sûr, ce n'est pas exactement la façon dont les nombres à virgule flottante sont stockés en mémoire (ils utilisent une forme de notation scientifique). Cependant, cela illustre le fait que les erreurs de précision en virgule flottante binaire ont tendance à apparaître parce que les nombres "réels" avec lesquels nous sommes généralement intéressés de travailler sont si souvent des puissances de dix - mais uniquement parce que nous utilisons un système de nombres décimaux jour- aujourd'hui. C'est aussi pourquoi nous dirons des choses comme 71% au lieu de "5 sur 7" (71% est une approximation, car 5/7 ne peut pas être représenté exactement avec un nombre décimal).
Donc non: les nombres à virgule flottante binaires ne sont pas cassés, ils se trouvent être aussi imparfaits que tous les autres systèmes de nombres en base-N :)
Side Side Note: Travailler avec des flottants dans la programmation
Dans la pratique, ce problème de précision signifie que vous devez utiliser des fonctions d'arrondi pour arrondir vos nombres à virgule flottante au nombre de décimales qui vous intéresse avant de les afficher.
Vous devez également remplacer les tests d'égalité par des comparaisons qui permettent une certaine tolérance, ce qui signifie:
Ne fais pasif (x == y) { ... }
Au lieu de cela if (abs(x - y) < myToleranceValue) { ... }
.
où abs
est la valeur absolue. myToleranceValue
doit être choisi pour votre application particulière - et cela aura beaucoup à voir avec la quantité de "marge de manœuvre" que vous êtes prêt à autoriser, et quel peut être le plus grand nombre que vous allez comparer (en raison de problèmes de perte de précision) ). Méfiez-vous des constantes de style "epsilon" dans la langue de votre choix. Ces valeurs ne doivent pas être utilisées comme valeurs de tolérance.
Je crois que je devrais ajouter une perspective de concepteur de matériel à cela puisque je conçois et fabrique du matériel à virgule flottante. Connaître l'origine de l'erreur peut aider à comprendre ce qui se passe dans le logiciel et, finalement, j'espère que cela aidera à expliquer les raisons pour lesquelles les erreurs à virgule flottante se produisent et semblent s'accumuler au fil du temps.
D'un point de vue technique, la plupart des opérations en virgule flottante comporteront un élément d'erreur, car le matériel qui effectue les calculs en virgule flottante ne doit avoir en dernier lieu qu'une erreur de moins de la moitié d'une unité. Par conséquent, une grande partie du matériel s'arrêtera à une précision qui n'est nécessaire que pour produire une erreur de moins de la moitié d'une unité en dernier lieu pour une seule opération, ce qui est particulièrement problématique dans la division en virgule flottante. Ce qui constitue une seule opération dépend du nombre d'opérandes que l'unité prend. Pour la plupart, c'est deux, mais certaines unités prennent 3 opérandes ou plus. Pour cette raison, il n'y a aucune garantie que des opérations répétées entraîneront une erreur souhaitable car les erreurs s'ajoutent au fil du temps.
La plupart des processeurs suivent la norme IEEE-754 mais certains utilisent des normes dénormalisées ou différentes. Par exemple, il existe un mode dénormalisé dans IEEE-754 qui permet la représentation de très petits nombres à virgule flottante au détriment de la précision. Cependant, ce qui suit couvrira le mode normalisé de l'IEEE-754 qui est le mode de fonctionnement typique.
Dans la norme IEEE-754, les concepteurs de matériel sont autorisés à toute valeur d'erreur / epsilon tant qu'il est inférieur à la moitié d'une unité à la dernière place, et que le résultat ne doit être inférieur à la moitié d'une unité que dans la dernière place pour une opération. Cela explique pourquoi lorsqu'il y a des opérations répétées, les erreurs s'additionnent. Pour la double précision IEEE-754, il s'agit du 54e bit, car 53 bits sont utilisés pour représenter la partie numérique (normalisée), également appelée mantisse, du nombre à virgule flottante (par exemple, le 5,3 en 5,3e5). Les sections suivantes détaillent les causes des erreurs matérielles sur diverses opérations en virgule flottante.
La principale cause de l'erreur dans la division en virgule flottante est les algorithmes de division utilisés pour calculer le quotient. La plupart des systèmes informatiques calculent la division en utilisant la multiplication par un inverse, principalement dans Z=X/Y
,Z = X * (1/Y)
. Une division est calculée de manière itérative, c'est-à-dire que chaque cycle calcule quelques bits du quotient jusqu'à ce que la précision souhaitée soit atteinte, ce qui pour IEEE-754 est n'importe quoi avec une erreur de moins d'une unité en dernier lieu. La table des inverses de Y (1 / Y) est connue sous le nom de table de sélection de quotient (QST) dans la division lente, et la taille en bits de la table de sélection de quotient est généralement la largeur du radix, ou un nombre de bits de le quotient calculé à chaque itération, plus quelques bits de garde. Pour la norme IEEE-754, double précision (64 bits), ce serait la taille du radix du diviseur, plus quelques bits de garde k, où k>=2
. Ainsi, par exemple, un tableau de sélection de quotient typique pour un diviseur qui calcule 2 bits du quotient à la fois (radix 4) serait des 2+2= 4
bits (plus quelques bits facultatifs).
3.1 Erreur d'arrondi de division: approximation de la réciprocité
Les inverses dans le tableau de sélection des quotients dépendent de la méthode de division : division lente telle que la division SRT ou division rapide telle que la division Goldschmidt; chaque entrée est modifiée selon l'algorithme de division pour tenter de générer l'erreur la plus faible possible. Dans tous les cas, cependant, tous les réciproques sont des approximationsde la réciproque réelle et introduire un élément d'erreur. Les méthodes de division lente et de division rapide calculent le quotient de manière itérative, c'est-à-dire qu'un certain nombre de bits du quotient sont calculés à chaque étape, puis le résultat est soustrait du dividende et le diviseur répète les étapes jusqu'à ce que l'erreur soit inférieure à la moitié d'un l'unité en dernier lieu. Les méthodes de division lente calculent un nombre fixe de chiffres du quotient à chaque étape et sont généralement moins coûteuses à construire, et les méthodes de division rapide calculent un nombre variable de chiffres par étape et sont généralement plus coûteuses à construire. La partie la plus importante des méthodes de division est que la plupart d'entre elles reposent sur une multiplication répétée par une approximation d'une réciproque, de sorte qu'elles sont sujettes à l'erreur.
Une autre cause des erreurs d'arrondi dans toutes les opérations est les différents modes de troncature de la réponse finale que permet IEEE-754. Il y a tronqué, arrondi à zéro, arrondi au plus proche (par défaut), arrondi et arrondi. Toutes les méthodes introduisent en dernier lieu un élément d'erreur inférieur à une unité pour une seule opération. Au fil du temps et des opérations répétées, la troncature ajoute également cumulativement à l'erreur résultante. Cette erreur de troncature est particulièrement problématique dans l'exponentiation, qui implique une certaine forme de multiplication répétée.
Étant donné que le matériel qui effectue les calculs en virgule flottante n'a besoin que de produire un résultat avec une erreur inférieure à la moitié d'une unité en dernier lieu pour une seule opération, l'erreur augmentera au fil des opérations répétées si elle n'est pas surveillée. C'est la raison pour laquelle dans les calculs qui nécessitent une erreur bornée, les mathématiciens utilisent des méthodes telles que l'utilisation du chiffre pair arrondi au plus proche à la dernière place de IEEE-754, car, au fil du temps, les erreurs sont plus susceptibles de s'annuler out, et Arithmetic Interval combiné avec des variations des modes d'arrondi IEEE 754pour prévoir les erreurs d'arrondi et les corriger. En raison de sa faible erreur relative par rapport aux autres modes d'arrondi, l'arrondi au chiffre pair le plus proche (en dernier lieu) est le mode d'arrondi par défaut de l'IEEE-754.
Notez que le mode d'arrondi par défaut, arrondi au chiffre pair le plus proche à la dernière place , garantit une erreur de moins de la moitié d'une unité à la dernière place pour une opération. L'utilisation de la troncature, de l'arrondi et de l'arrondi seuls peut entraîner une erreur supérieure à la moitié d'une unité à la dernière place, mais inférieure à une unité à la dernière place, ces modes ne sont donc pas recommandés à moins qu'ils ne le soient utilisé en arithmétique d'intervalle.
En bref, la raison fondamentale des erreurs dans les opérations en virgule flottante est une combinaison de la troncature dans le matériel et de la troncature d'une réciproque dans le cas de la division. Étant donné que la norme IEEE-754 ne requiert qu'une erreur de moins de la moitié d'une unité à la dernière place pour une seule opération, les erreurs en virgule flottante sur les opérations répétées s'additionneront sauf si elles sont corrigées.
Lorsque vous convertissez 0,1 ou 1/10 en base 2 (binaire), vous obtenez un motif répétitif après le point décimal, tout comme essayer de représenter 1/3 en base 10. La valeur n'est pas exacte, et donc vous ne pouvez pas faire calcul exact avec elle en utilisant des méthodes normales en virgule flottante.
La plupart des réponses ici abordent cette question en termes techniques très secs. Je voudrais aborder cela en termes que les êtres humains normaux peuvent comprendre.
Imaginez que vous essayez de couper des pizzas. Vous avez un coupe-pizza robotisé qui peut couper les tranches de pizza exactement de moitié. Il peut réduire de moitié une pizza entière, ou il peut diviser par deux une tranche existante, mais dans tous les cas, la réduction de moitié est toujours exacte.
Ce coupe-pizza a des mouvements très fins, et si vous commencez avec une pizza entière, puis la divisez en deux et continuez de diviser la plus petite tranche à chaque fois, vous pouvez effectuer la division de 53 fois avant que la tranche ne soit trop petite pour ses capacités de haute précision . À ce stade, vous ne pouvez plus diviser par deux cette tranche très mince, mais vous devez l'inclure ou l'exclure telle quelle.
Maintenant, comment décomposeriez-vous toutes les tranches de manière à ce que cela représente un dixième (0,1) ou un cinquième (0,2) d'une pizza? Pensez-y vraiment, et essayez de le résoudre. Vous pouvez même essayer d'utiliser une vraie pizza, si vous avez un coupe-pizza de précision mythique à portée de main. :-)
La plupart des programmeurs expérimentés connaissent bien sûr la vraie réponse, à savoir qu'il n'y a aucun moyen de reconstituer exactement un dixième ou un cinquième de la pizza en utilisant ces tranches, quelle que soit la finesse de vos tranches. Vous pouvez faire une assez bonne approximation, et si vous additionnez l'approximation de 0,1 à l'approximation de 0,2, vous obtenez une assez bonne approximation de 0,3, mais c'est toujours juste cela, une approximation.
Pour les nombres à double précision (qui est la précision qui vous permet de diviser par deux votre pizza 53 fois), les nombres immédiatement inférieurs et supérieurs à 0,1 sont 0,0999999999999999999167332731531132594682276248931884765625 et 0,1000000000000000055511151231257827021181583404541015625. Ce dernier est un peu plus proche de 0,1 que le premier, donc un analyseur numérique sera, compte tenu d'une entrée de 0,1, favoriser le second.
(La différence entre ces deux nombres est la "plus petite tranche" que nous devons décider d'inclure, ce qui introduit un biais vers le haut, ou d'exclure, ce qui introduit un biais vers le bas. Le terme technique pour cette plus petite tranche est un ulp .)
Dans le cas de 0,2, les chiffres sont tous les mêmes, juste augmentés d'un facteur de 2. Encore une fois, nous privilégions la valeur légèrement supérieure à 0,2.
Notez que dans les deux cas, les approximations de 0,1 et 0,2 ont un léger biais à la hausse. Si nous ajoutons suffisamment de ces biais, ils repousseront le nombre de plus en plus loin de ce que nous voulons, et en fait, dans le cas de 0,1 + 0,2, le biais est suffisamment élevé pour que le nombre résultant ne soit plus le nombre le plus proche. à 0,3.
En particulier, 0,1 + 0,2 + est vraiment 0.1000000000000000055511151231257827021181583404541015625 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, alors que le nombre le plus proche de 0,3 est en fait 0,299999999999999988897769753748434595763683319091796875.
PS Certains langages de programmation fournissent également des coupe-pizza qui peuvent diviser les tranches en dixièmes exacts . Bien que de tels coupe-pizzas soient rares, si vous en avez un, vous devez l'utiliser quand il est important de pouvoir obtenir exactement un dixième ou un cinquième d'une tranche.
Erreurs d'arrondi à virgule flottante. 0,1 ne peut pas être représenté aussi précisément en base 2 qu'en base 10 en raison du facteur premier manquant de 5. Tout comme 1/3 prend un nombre infini de chiffres à représenter en décimal, mais est "0,1" en base-3, 0,1 prend un nombre infini de chiffres en base-2 alors qu'il ne le fait pas en base-10. Et les ordinateurs n'ont pas une quantité infinie de mémoire.
En plus des autres bonnes réponses, vous pouvez envisager de mettre à l'échelle vos valeurs pour éviter les problèmes d'arithmétique à virgule flottante.
Par exemple:
var result = 1.0 + 2.0; // result === 3.0 returns true
... au lieu de:
var result = 0.1 + 0.2; // result === 0.3 returns false
L'expression 0.1 + 0.2 === 0.3
revient false
en JavaScript, mais heureusement l'arithmétique entière en virgule flottante est exacte, de sorte que les erreurs de représentation décimale peuvent être évitées en mettant à l'échelle.
À titre d'exemple pratique, pour éviter les problèmes de virgule flottante où la précision est primordiale, il est recommandé 1 de gérer l'argent comme un entier représentant le nombre de cents: 2550
cents au lieu de 25.50
dollars.
1 Douglas Crockford: JavaScript: The Good Parts : Annexe A - Awful Parts (page 105) .
Ma réponse est assez longue, je l'ai donc divisée en trois sections. Puisque la question concerne les mathématiques à virgule flottante, j'ai mis l'accent sur ce que fait réellement la machine. Je l'ai également rendu spécifique à la précision double (64 bits), mais l'argument s'applique également à toute arithmétique à virgule flottante.
Préambule
Un nombre au format à virgule flottante binaire double précision IEEE 754 (binaire64) représente un numéro de la forme
valeur = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023
en 64 bits:
1
si le nombre est négatif, 0
sinon 1 .1.
est toujours 2 omis puisque le bit le plus significatif de toute valeur binaire est 1
.1 - IEEE 754 permet le concept d'un zéro signé - +0
et -0
sont traités différemment: 1 / (+0)
est l'infini positif; 1 / (-0)
est l'infini négatif. Pour les valeurs nulles, les bits de mantisse et d'exposant sont tous nuls. Remarque: les valeurs nulles (+0 et -0) ne sont explicitement pas classées comme dénormales 2 .
2 - Ce n'est pas le cas pour les nombres dénormaux , qui ont un exposant de décalage de zéro (et un implicite 0.
). La plage des nombres dénormaux à double précision est d min ≤ | x | ≤ d max , où d min (le plus petit nombre non nul représentable) est 2 -1023 - 51 (≈ 4,94 * 10 -324 ) et d max (le plus grand nombre dénormal, pour lequel la mantisse est entièrement composée de 1
s) est 2 -1023 + 1 - 2 - 1023 - 51 (≈ 2,225 * 10 - 308 ).
Transformer un nombre double précision en binaire
De nombreux convertisseurs en ligne existent pour convertir un nombre à virgule flottante double précision en binaire (par exemple sur binaryconvert.com ), mais voici un exemple de code C # pour obtenir la représentation IEEE 754 pour un nombre à double précision (je sépare les trois parties par des deux-points ( :
) :
public static string BinaryRepresentation(double value)
{
long valueInLongType = BitConverter.DoubleToInt64Bits(value);
string bits = Convert.ToString(valueInLongType, 2);
string leadingZeros = new string('0', 64 - bits.Length);
string binaryRepresentation = leadingZeros + bits;
string sign = binaryRepresentation[0].ToString();
string exponent = binaryRepresentation.Substring(1, 11);
string mantissa = binaryRepresentation.Substring(12);
return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}
Aller droit au but: la question d'origine
(Passer au bas pour la version TL; DR)
Cato Johnston (le poseur de questions) a demandé pourquoi 0,1 + 0,2! = 0,3.
Ecrit en binaire (avec deux points séparant les trois parties), les représentations IEEE 754 des valeurs sont:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
Notez que la mantisse est composée de chiffres récurrents de 0011
. Ceci est essentiel à la raison pour laquelle il y a une erreur dans les calculs - 0,1, 0,2 et 0,3 ne peuvent être représentés en binaire avec précision dans un fini nombre de bits binaires , pas plus que 1/9, 1/3 ou 1/7 peuvent être représentées avec précision dans chiffres décimaux .
Notez également que nous pouvons diminuer la puissance de l'exposant de 52 et déplacer le point de la représentation binaire vers la droite de 52 endroits (un peu comme 10 -3 * 1.23 == 10 -5 * 123). Cela nous permet alors de représenter la représentation binaire comme la valeur exacte qu'elle représente sous la forme a * 2 p . où 'a' est un entier.
La conversion des exposants en décimales, la suppression de l'offset et le rajout des valeurs implicites 1
(entre crochets), 0,1 et 0,2 sont:
0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
Pour ajouter deux nombres, l'exposant doit être le même, c'est-à-dire:
0.1 => 2^-3 * 0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 * 1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
sum = 2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875
Puisque la somme n'est pas de la forme 2 n * 1. {bbb}, nous augmentons l'exposant de un et décalons le point décimal ( binaire ) pour obtenir:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
Il y a maintenant 53 bits dans la mantisse (le 53e est entre crochets dans la ligne ci-dessus). Le mode d'arrondi par défaut pour IEEE 754 est ' Arrondir au plus proche ' - c'est-à-dire que si un nombre x se situe entre deux valeurs a et b , la valeur où le bit le moins significatif est zéro est choisie.
a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
= 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
Notez que a et b ne diffèrent que dans le dernier bit; ...0011
+ 1
= ...0100
. Dans ce cas, la valeur avec le bit le moins significatif de zéro est b , donc la somme est:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
alors que la représentation binaire de 0,3 est:
0.3 => 2^-2 * 1.0011001100110011001100110011001100110011001100110011
= 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
qui ne diffère que de la représentation binaire de la somme de 0,1 et 0,2 par 2 -54 .
Les représentations binaires de 0,1 et 0,2 sont les représentations les plus précises des nombres autorisés par IEEE 754. L'ajout de ces représentations, en raison du mode d'arrondi par défaut, donne une valeur qui ne diffère que par le bit le moins significatif.
TL; DR
Écrire 0.1 + 0.2
dans une représentation binaire IEEE 754 (avec deux points séparant les trois parties) et la comparer à 0.3
, c'est (j'ai mis les bits distincts entre crochets):
0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3 => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
Reconverties en décimales, ces valeurs sont:
0.1 + 0.2 => 0.300000000000000044408920985006...
0.3 => 0.299999999999999988897769753748...
La différence est exactement 2 -54 , ce qui est ~ 5,5511151231258 × 10 -17 - insignifiant (pour de nombreuses applications) par rapport aux valeurs d'origine.
La comparaison des derniers bits d'un nombre à virgule flottante est intrinsèquement dangereuse, comme le sait quiconque lit le fameux " Ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante " (qui couvre toutes les parties principales de cette réponse) le sait.
La plupart des calculatrices utilisent des chiffres de garde supplémentaires pour contourner ce problème, ce 0.1 + 0.2
qui donne 0.3
: les derniers bits sont arrondis.
Les nombres à virgule flottante stockés dans l'ordinateur se composent de deux parties, un entier et un exposant vers lequel la base est prise et multipliée par la partie entière.
Si l'ordinateur fonctionnait en base 10, ce 0.1
serait 1 x 10⁻¹
, ce 0.2
serait 2 x 10⁻¹
et ce 0.3
serait 3 x 10⁻¹
. Les mathématiques entières sont faciles et exactes, donc l'ajout 0.1 + 0.2
se traduira évidemment par 0.3
.
Les ordinateurs ne fonctionnent généralement pas en base 10, ils fonctionnent en base 2. Vous pouvez toujours obtenir des résultats exacts pour certaines valeurs, par exemple 0.5
est 1 x 2⁻¹
et 0.25
est 1 x 2⁻²
, et en les ajoutant 3 x 2⁻²
, ou 0.75
. Exactement.
Le problème vient des nombres qui peuvent être représentés exactement en base 10, mais pas en base 2. Ces nombres doivent être arrondis à leur équivalent le plus proche. En supposant le format à virgule flottante IEEE 64 bits très courant, le nombre le plus proche de 0.1
est 3602879701896397 x 2⁻⁵⁵
et le nombre le plus proche de 0.2
est 7205759403792794 x 2⁻⁵⁵
; les ajouter ensemble donne 10808639105689191 x 2⁻⁵⁵
une valeur décimale exacte de 0.3000000000000000444089209850062616169452667236328125
. Les nombres à virgule flottante sont généralement arrondis pour l'affichage.
Erreur d'arrondi en virgule flottante. De ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante :
La compression d'une infinité de nombres réels en un nombre fini de bits nécessite une représentation approximative. Bien qu'il existe une infinité de nombres entiers, dans la plupart des programmes, le résultat des calculs d'entiers peut être stocké sur 32 bits. En revanche, étant donné un nombre fixe de bits, la plupart des calculs avec des nombres réels produisent des quantités qui ne peuvent pas être représentées exactement en utilisant autant de bits. Par conséquent, le résultat d'un calcul en virgule flottante doit souvent être arrondi afin de se réinsérer dans sa représentation finie. Cette erreur d'arrondi est la caractéristique du calcul en virgule flottante.
Beaucoup de bonnes réponses ont été publiées, mais j'aimerais en ajouter une de plus.
Tous les nombres ne peuvent pas être représentés via des flottants / doubles Par exemple, le nombre "0,2" sera représenté par "0.200000003" en simple précision dans la norme de point flottant IEEE754.
Le modèle pour stocker des nombres réels sous le capot représente des nombres flottants comme
Même si vous pouvez taper 0.2
facilement FLT_RADIX
et DBL_RADIX
vaut 2; pas 10 pour un ordinateur avec FPU qui utilise la "Norme IEEE pour l'arithmétique à virgule flottante binaire (ISO / IEEE Std 754-1985)".
Il est donc un peu difficile de représenter exactement de tels nombres. Même si vous spécifiez explicitement cette variable sans aucun calcul intermédiaire.
Quelques statistiques liées à cette fameuse question de double précision.
Lors de l'ajout de toutes les valeurs ( a + b ) en utilisant un pas de 0,1 (de 0,1 à 100), nous avons ~ 15% de risque d'erreur de précision . Notez que l'erreur peut entraîner des valeurs légèrement plus grandes ou plus petites. Voici quelques exemples:
0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)
En soustrayant toutes les valeurs ( a - b où a> b ) en utilisant un pas de 0,1 (de 100 à 0,1), nous avons ~ 34% de chances d'erreur de précision . Voici quelques exemples:
0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)
* 15% et 34% sont en effet énormes, utilisez donc toujours BigDecimal lorsque la précision est d'une grande importance. Avec 2 chiffres décimaux (étape 0.01), la situation s'aggrave un peu plus (18% et 36%).
Sommaire
L'arithmétique en virgule flottante est exacte, malheureusement, elle ne correspond pas bien à notre représentation habituelle des nombres en base 10, il s'avère donc que nous lui donnons souvent une entrée légèrement différente de ce que nous avons écrit.
Même des nombres simples comme 0,01, 0,02, 0,03, 0,04 ... 0,24 ne sont pas représentables exactement comme des fractions binaires. Si vous comptez 0,01, 0,02, 0,03 ..., ce n'est qu'après avoir atteint 0,25 que vous obtiendrez la première fraction représentable en base 2 . Si vous avez essayé cela en utilisant FP, votre 0,01 aurait été légèrement désactivé, donc la seule façon d'en ajouter 25 jusqu'à une bonne 0,25 aurait exigé une longue chaîne de causalité impliquant des bits de garde et des arrondis. C'est difficile à prévoir, alors nous lâchons nos mains et disons "FP is inexact", mais ce n'est pas vraiment vrai.
Nous donnons constamment au matériel FP quelque chose qui semble simple en base 10 mais qui est une fraction répétitive en base 2.
Comment est-ce arrivé?
Lorsque nous écrivons en décimal, chaque fraction (en particulier, chaque décimale terminale) est un nombre rationnel de la forme
a / (2 n x 5 m )
En binaire, nous n'obtenons que le terme 2 n , c'est-à-dire:
a / 2 n
Donc , en décimal, on ne peut pas représenter 1 / 3 . Parce que la base 10 comprend 2 comme facteur premier, chaque nombre que nous pouvons écrire comme fraction binaire peut également être écrit comme fraction de base 10. Cependant, presque rien que nous écrivons en tant que fraction de base 10 n'est représentable en binaire. Dans la plage de 0,01, 0,02, 0,03 ... 0,99, seuls trois nombres peuvent être représentés dans notre format FP: 0,25, 0,50 et 0,75, car ils sont 1/4, 1/2 et 3/4, tous les nombres avec un facteur premier utilisant uniquement le terme 2 n .
Dans la base 10 , nous ne pouvons pas représenter 1 / 3 . Mais en binaire, nous ne pouvons pas 1 / 10 ou 1 / 3 .
Ainsi, alors que chaque fraction binaire peut être écrite en décimal, l'inverse n'est pas vrai. Et en fait, la plupart des fractions décimales se répètent en binaire.
Comment y faire face
Les développeurs sont généralement chargés de faire des comparaisons <epsilon , un meilleur conseil pourrait être d'arrondir aux valeurs intégrales (dans la bibliothèque C: round () et roundf (), c'est-à-dire de rester au format FP), puis de comparer. L'arrondi à une longueur de fraction décimale spécifique résout la plupart des problèmes de sortie.
De plus, sur les vrais problèmes de calcul des nombres (les problèmes pour lesquels FP a été inventé sur les premiers ordinateurs terriblement chers), les constantes physiques de l'univers et toutes les autres mesures ne sont connues que d'un nombre relativement petit de chiffres significatifs, donc tout l'espace du problème était "inexact" de toute façon. La «précision» de FP n'est pas un problème dans ce type d'application.
Tout le problème se pose vraiment lorsque les gens essaient d'utiliser la FP pour le comptage des haricots. Cela fonctionne pour cela, mais seulement si vous vous en tenez aux valeurs intégrales, ce qui défait le point de l'utiliser. C'est pourquoi nous avons toutes ces bibliothèques de logiciels de fraction décimale.
J'adore la réponse de Pizza de Chris , car elle décrit le problème réel, pas seulement le geste habituel de "l'inexactitude". Si la PF était simplement "inexacte", nous pourrions corriger cela et l'aurions fait il y a des décennies. La raison pour laquelle nous ne l'avons pas est parce que le format FP est compact et rapide et c'est la meilleure façon de croquer beaucoup de nombres. C'est aussi un héritage de l'ère spatiale et de la course aux armements et des premières tentatives pour résoudre de gros problèmes avec des ordinateurs très lents utilisant de petits systèmes de mémoire. (Parfois, des noyaux magnétiques individuels pour le stockage 1 bit, mais c'est une autre histoire. )
Conclusion
Si vous comptez simplement des beans dans une banque, les solutions logicielles qui utilisent en premier lieu des représentations de chaînes décimales fonctionnent parfaitement. Mais vous ne pouvez pas faire la chromodynamique quantique ou l'aérodynamique de cette façon.
nextafter()
avec un incrément ou une décrémentation entière sur la représentation binaire d'un flottant IEEE. En outre, vous pouvez comparer les flottants sous forme d'entiers et obtenir la bonne réponse, sauf lorsqu'ils sont tous les deux négatifs (en raison de l'amplitude des signes par rapport au complément à 2).
Avez-vous essayé la solution de ruban adhésif?
Essayez de déterminer quand des erreurs se produisent et corrigez-les avec des instructions if courtes, ce n'est pas joli, mais pour certains problèmes, c'est la seule solution et c'est l'une d'entre elles.
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
J'ai eu le même problème dans un projet de simulation scientifique en c #, et je peux vous dire que si vous ignorez l'effet papillon, ça va se transformer en un gros dragon gras et vous mordre dans le a **
Afin d'offrir la meilleure solution, je peux dire que j'ai découvert la méthode suivante:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
Permettez-moi d'expliquer pourquoi c'est la meilleure solution. Comme d'autres l'ont mentionné dans les réponses ci-dessus, c'est une bonne idée d'utiliser la fonction Javascript toFixed () prête à l'emploi pour résoudre le problème. Mais vous rencontrerez probablement des problèmes.
Imaginez que vous allez ajouter deux numéros de flotteur comme 0.2
et 0.7
voici: 0.2 + 0.7 = 0.8999999999999999
.
Votre résultat attendu était que 0.9
cela signifie que vous avez besoin d'un résultat avec une précision à 1 chiffre dans ce cas. Vous devriez donc avoir utilisé (0.2 + 0.7).tofixed(1)
mais vous ne pouvez pas simplement donner un certain paramètre à toFixed () car cela dépend du nombre donné, par exemple
`0.22 + 0.7 = 0.9199999999999999`
Dans cet exemple, vous avez besoin d'une précision de 2 chiffres toFixed(2)
, ce qui devrait être le cas, alors quel devrait être le paramètre pour s'adapter à chaque nombre flottant donné?
Vous pourriez dire que ce soit 10 dans chaque situation:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
Zut! Qu'allez-vous faire avec ces zéros indésirables après 9? C'est le moment de le convertir en flotteur pour le faire comme vous le souhaitez:
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9
Maintenant que vous avez trouvé la solution, il est préférable de l'offrir comme une fonction comme celle-ci:
function floatify(number){
return parseFloat((number).toFixed(10));
}
Essayons vous-même:
function floatify(number){
return parseFloat((number).toFixed(10));
}
function addUp(){
var number1 = +$("#number1").val();
var number2 = +$("#number2").val();
var unexpectedResult = number1 + number2;
var expectedResult = floatify(number1 + number2);
$("#unexpectedResult").text(unexpectedResult);
$("#expectedResult").text(expectedResult);
}
addUp();
input{
width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>
Vous pouvez l'utiliser de cette façon:
var x = 0.2 + 0.7;
floatify(x); => Result: 0.9
Comme W3SCHOOLS suggère qu'il existe également une autre solution, vous pouvez multiplier et diviser pour résoudre le problème ci-dessus:
var x = (0.2 * 10 + 0.1 * 10) / 10; // x will be 0.3
Gardez à l'esprit que (0.2 + 0.1) * 10 / 10
cela ne fonctionnera pas du tout, même si cela semble le même! Je préfère la première solution car je peux l'appliquer comme une fonction qui convertit le flotteur d'entrée en flotteur de sortie précis.
Ces nombres étranges apparaissent parce que les ordinateurs utilisent le système numérique binaire (base 2) à des fins de calcul, tandis que nous utilisons décimal (base 10).
Il existe une majorité de nombres fractionnaires qui ne peuvent être représentés précisément ni en binaire ni en décimal ou les deux. Résultat - Un nombre arrondi (mais précis) résulte.
De nombreux doublons de cette question portent sur les effets de l'arrondi à virgule flottante sur des nombres spécifiques. En pratique, il est plus facile de se faire une idée de son fonctionnement en regardant les résultats exacts des calculs d'intérêt plutôt qu'en lisant simplement à ce sujet. Certains langages offrent des moyens de le faire - comme la conversion d'un float
ou double
vers BigDecimal
en Java.
Comme il s'agit d'une question indépendante de la langue, elle a besoin d'outils indépendants de la langue, tels qu'un convertisseur décimal en virgule flottante .
En l'appliquant aux nombres de la question, traités comme des doubles:
0,1 convertit en 0,0000000000000000000055511151231257827021181583404541015625,
0,2 convertit en 0,200000000000000011102230246251565404236316680908203125,
0,3 se transforme en 0,29999999999999999988897769753748434595763683319091796875 et
0.30000000000000004 se convertit en 0.3000000000000000444089209850062616169452667236328125.
L'ajout des deux premiers nombres manuellement ou dans une calculatrice décimale telle que la calculatrice de précision complète , montre que la somme exacte des entrées réelles est de 0,3000000000000000166533453693773481063544750213623046875.
S'il était arrondi à l'équivalent de 0,3, l'erreur d'arrondi serait de 0,000000000000000000277555756156289135105907917022705078125. L'arrondi à l'équivalent de 0,30000000000000004 donne également une erreur d'arrondi 0,0000000000000000277555756156289135105907917022705078125. Le disjoncteur égal à égal s'applique.
En revenant au convertisseur à virgule flottante, l'hexadécimal brut pour 0,30000000000000004 est 3fd3333333333334, qui se termine par un chiffre pair et est donc le résultat correct.
Étant donné que personne n'a mentionné cela ...
Certains langages de haut niveau tels que Python et Java sont livrés avec des outils pour surmonter les limitations binaires en virgule flottante. Par exemple:
decimal
Module Python et BigDecimal
classe Java , qui représentent les nombres en interne avec une notation décimale (par opposition à la notation binaire). Les deux ont une précision limitée, ils sont donc toujours sujets aux erreurs, mais ils résolvent la plupart des problèmes courants avec l'arithmétique binaire à virgule flottante.
Les décimales sont très bien quand il s'agit d'argent: dix cents plus vingt cents sont toujours exactement trente cents:
>>> 0.1 + 0.2 == 0.3
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
Le decimal
module de Python est basé sur la norme IEEE 854-1987 .
fractions
Module Python et BigFraction
classe Apache Common . Les deux représentent des nombres rationnels sous forme de (numerator, denominator)
paires et peuvent donner des résultats plus précis que l'arithmétique décimale à virgule flottante.
Aucune de ces solutions n'est parfaite (surtout si nous regardons les performances, ou si nous avons besoin d'une très haute précision), mais elles résolvent toujours un grand nombre de problèmes avec l'arithmétique binaire à virgule flottante.
Puis-je simplement ajouter; les gens supposent toujours que c'est un problème informatique, mais si vous comptez avec vos mains (base 10), vous ne pouvez pas obtenir à (1/3+1/3=2/3)=true
moins d'avoir l'infini pour ajouter 0,333 ... à 0,333 ... donc tout comme avec le (1/10+2/10)!==3/10
problème de base 2, vous le tronquez à 0,333 + 0,333 = 0,666 et l'arrondissez probablement à 0,667, ce qui serait également techniquement inexact.
Comptez en ternaire, et les tiers ne sont pas un problème cependant - peut-être qu'une course avec 15 doigts sur chaque main demanderait pourquoi votre calcul décimal a été cassé ...
Le type de calcul à virgule flottante qui peut être implémenté dans un ordinateur numérique utilise nécessairement une approximation des nombres réels et des opérations sur ceux-ci. (La version standard comprend plus de cinquante pages de documentation et dispose d'un comité pour traiter ses errata et les affiner.)
Cette approximation est un mélange d'approximations de différents types, dont chacun peut être ignoré ou soigneusement pris en compte en raison de sa manière spécifique de s'écarter de l'exactitude. Cela implique également un certain nombre de cas exceptionnels explicites au niveau matériel et logiciel que la plupart des gens passent devant tout en faisant semblant de ne pas remarquer.
Si vous avez besoin d'une précision infinie (en utilisant le nombre π, par exemple, au lieu de l'un de ses nombreux remplaçants plus courts), vous devez écrire ou utiliser un programme mathématique symbolique à la place.
Mais si vous êtes d'accord avec l'idée que parfois les mathématiques à virgule flottante ont une valeur floue et que la logique et les erreurs peuvent s'accumuler rapidement, et que vous pouvez écrire vos exigences et vos tests pour permettre cela, alors votre code peut souvent se débrouiller avec ce qui est dedans. votre FPU.
Juste pour le plaisir, j'ai joué avec la représentation des flotteurs, en suivant les définitions de la norme C99 et j'ai écrit le code ci-dessous.
Le code imprime la représentation binaire des flottants en 3 groupes séparés
SIGN EXPONENT FRACTION
et après cela, il imprime une somme qui, une fois additionnée avec suffisamment de précision, montrera la valeur qui existe réellement dans le matériel.
Ainsi, lorsque vous écrivez float x = 999...
, le compilateur transformera ce nombre en une représentation binaire imprimée par la fonction de xx
telle sorte que la somme imprimée par la fonction yy
soit égale au nombre donné.
En réalité, cette somme n'est qu'une approximation. Pour le nombre 999 999 999, le compilateur insérera dans la représentation en bits du flotteur le nombre 1 000 000 000
Après le code, je joins une session de console, dans laquelle je calcule la somme des termes pour les deux constantes (moins PI et 999999999) qui existent vraiment dans le matériel, insérées là par le compilateur.
#include <stdio.h>
#include <limits.h>
void
xx(float *x)
{
unsigned char i = sizeof(*x)*CHAR_BIT-1;
do {
switch (i) {
case 31:
printf("sign:");
break;
case 30:
printf("exponent:");
break;
case 23:
printf("fraction:");
break;
}
char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
printf("%d ", b);
} while (i--);
printf("\n");
}
void
yy(float a)
{
int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
int fraction = ((1<<23)-1)&(*(int*)&a);
int exponent = (255&((*(int*)&a)>>23))-127;
printf(sign?"positive" " ( 1+":"negative" " ( 1+");
unsigned int i = 1<<22;
unsigned int j = 1;
do {
char b=(fraction&i)!=0;
b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
} while (j++, i>>=1);
printf("*2^%d", exponent);
printf("\n");
}
void
main()
{
float x=-3.14;
float y=999999999;
printf("%lu\n", sizeof(x));
xx(&x);
xx(&y);
yy(x);
yy(y);
}
Voici une session console dans laquelle je calcule la valeur réelle du flotteur qui existe dans le matériel. J'avais l'habitude bc
d'imprimer la somme des termes produits par le programme principal. On peut également insérer cette somme en python repl
ou quelque chose de similaire.
-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872
C'est ça. La valeur de 999999999 est en fait
999999999.999999446351872
Vous pouvez également vérifier bc
que -3.14 est également perturbé. N'oubliez pas de définir un scale
facteur bc
.
La somme affichée est à l'intérieur du matériel. La valeur que vous obtenez en la calculant dépend de l'échelle que vous définissez. J'ai mis le scale
facteur à 15. Mathématiquement, avec une précision infinie, il semble que ce soit 1 000 000 000.
Une autre façon de voir les choses: 64 bits sont utilisés pour représenter les nombres. En conséquence, il n'y a aucun moyen que plus de 2 ** 64 = 18 446 744 073 709 551 616 nombres différents puissent être représentés avec précision.
Cependant, Math dit qu'il existe déjà une infinité de décimales entre 0 et 1. IEE 754 définit un codage pour utiliser ces 64 bits efficacement pour un espace numérique beaucoup plus grand plus NaN et +/- Infinity, donc il y a des écarts entre les nombres représentés avec précision remplis de les chiffres sont approximatifs.
Malheureusement, 0,3 se trouve dans un écart.
Imaginez travailler en base dix avec, disons, 8 chiffres de précision. Vous vérifiez si
1/3 + 2 / 3 == 1
et apprenez que cela revient false
. Pourquoi? Eh bien, en tant que nombres réels, nous avons
1/3 = 0,333 .... et 2/3 = 0,666 ....
Troncature à huit décimales, on obtient
0.33333333 + 0.66666666 = 0.99999999
ce qui est bien sûr différent de 1.00000000
exactement 0.00000001
.
La situation pour les nombres binaires avec un nombre fixe de bits est exactement analogue. En chiffres réels, nous avons
1/10 = 0,0001100110011001100 ... (base 2)
et
1/5 = 0,0011001100110011001 ... (base 2)
Si nous les tronquions, disons, à sept bits, nous obtiendrions
0.0001100 + 0.0011001 = 0.0100101
tandis que d'autre part,
3/10 = 0,01001100110011 ... (base 2)
qui, tronquée à sept bits, est 0.0100110
, et ceux-ci diffèrent exactement 0.0000001
.
La situation exacte est légèrement plus subtile car ces nombres sont généralement stockés en notation scientifique. Ainsi, par exemple, au lieu de stocker 1/10 car 0.0001100
nous pouvons le stocker comme quelque chose comme 1.10011 * 2^-4
, en fonction du nombre de bits que nous avons alloués pour l'exposant et la mantisse. Cela affecte le nombre de chiffres de précision que vous obtenez pour vos calculs.
Le résultat est qu'en raison de ces erreurs d'arrondi, vous ne voulez essentiellement jamais utiliser == sur les nombres à virgule flottante. Au lieu de cela, vous pouvez vérifier si la valeur absolue de leur différence est inférieure à un petit nombre fixe.
Depuis Python 3.5, vous pouvez utiliser la math.isclose()
fonction pour tester l'égalité approximative:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
Étant donné que ce fil s'est ramifié un peu dans une discussion générale sur les implémentations en virgule flottante actuelles, j'ajouterais qu'il existe des projets pour résoudre leurs problèmes.
Jetez un œil à https://posithub.org/ par exemple, qui présente un type de nombre appelé posit (et son prédécesseur unum) qui promet d'offrir une meilleure précision avec moins de bits. Si ma compréhension est correcte, elle résout également le type de problèmes dans la question. Projet assez intéressant, la personne derrière est un mathématicien, le Dr John Gustafson . Le tout est open source, avec de nombreuses implémentations réelles en C / C ++, Python, Julia et C # ( https://hastlayer.com/arithmetics ).
C'est en fait assez simple. Lorsque vous avez un système de base 10 (comme le nôtre), il ne peut exprimer que des fractions qui utilisent un facteur premier de la base. Les facteurs premiers de 10 sont 2 et 5. Ainsi, 1/2, 1/4, 1/5, 1/8 et 1/10 peuvent tous être exprimés proprement car les dénominateurs utilisent tous des facteurs premiers de 10. En revanche, 1 / 3, 1/6 et 1/7 sont tous des décimales répétitives car leurs dénominateurs utilisent un facteur premier de 3 ou 7. En binaire (ou base 2), le seul facteur premier est 2. Ainsi, vous ne pouvez exprimer des fractions que ne contient que 2 comme facteur premier. En binaire, 1/2, 1/4, 1/8 seraient tous exprimés proprement en décimales. Tandis que 1/5 ou 1/10 répéteraient des décimales. Donc 0,1 et 0,2 (1/10 et 1/5), tout en décimales propres dans un système base 10, sont des décimales répétitives dans le système base 2 sur lequel l'ordinateur fonctionne. Lorsque vous faites des calculs sur ces décimales répétitives,
Les nombres décimaux tels que 0.1
, 0.2
et 0.3
ne sont pas représentés exactement dans les types à virgule flottante codés binaires. La somme des approximations pour 0.1
et 0.2
diffère de l'approximation utilisée pour 0.3
, d'où le mensonge de 0.1 + 0.2 == 0.3
as peut être vu plus clairement ici:
#include <stdio.h>
int main() {
printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
printf("0.1 is %.23f\n", 0.1);
printf("0.2 is %.23f\n", 0.2);
printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
printf("0.3 is %.23f\n", 0.3);
printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
return 0;
}
Production:
0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17
Pour que ces calculs soient évalués de manière plus fiable, vous devez utiliser une représentation décimale pour les valeurs à virgule flottante. La norme C ne spécifie pas de tels types par défaut mais comme une extension décrite dans un rapport technique .
Les _Decimal32
, _Decimal64
et les _Decimal128
types peuvent être disponibles sur votre système (par exemple, GCC les prend en charge sur des cibles sélectionnées , mais Clang ne les prend pas en charge sur OS X ).
Math.sum (javascript) .... type de remplacement d'opérateur
.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
Object.defineProperties(Math, {
sign: {
value: function (x) {
return x ? x < 0 ? -1 : 1 : 0;
}
},
precision: {
value: function (value, precision, type) {
var v = parseFloat(value),
p = Math.max(precision, 0) || 0,
t = type || 'round';
return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
}
},
scientific_to_num: { // this is from https://gist.github.com/jiggzson
value: function (num) {
//if the number is in scientific notation remove it
if (/e/i.test(num)) {
var zero = '0',
parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
e = parts.pop(), //store the exponential part
l = Math.abs(e), //get the number of zeros
sign = e / l,
coeff_array = parts[0].split('.');
if (sign === -1) {
num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
} else {
var dec = coeff_array[1];
if (dec)
l = l - dec.length;
num = coeff_array.join('') + new Array(l + 1).join(zero);
}
}
return num;
}
}
get_precision: {
value: function (number) {
var arr = Math.scientific_to_num((number + "")).split(".");
return arr[1] ? arr[1].length : 0;
}
},
sum: {
value: function () {
var prec = 0, sum = 0;
for (var i = 0; i < arguments.length; i++) {
prec = this.max(prec, this.get_precision(arguments[i]));
sum += +arguments[i]; // force float to convert strings to number
}
return Math.precision(sum, prec);
}
}
});
l'idée est d'utiliser des opérateurs mathématiques à la place pour éviter les erreurs flottantes
Math.sum détecte automatiquement la précision à utiliser
Math.sum accepte n'importe quel nombre d'arguments
Considérez les résultats suivants:
error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1
Nous pouvons clairement voir un point d'arrêt quand 2**53+1
- tout fonctionne bien jusqu'à 2**53
.
>>> (2**53) - int(float(2**53))
0
Cela se produit à cause du binaire double précision: format à virgule flottante binaire double précision IEEE 754: binaire64
À partir de la page Wikipedia pour le format à virgule flottante double précision :
La virgule flottante binaire à double précision est un format couramment utilisé sur les PC, en raison de sa gamme plus large sur la virgule flottante à simple précision, malgré ses performances et son coût de bande passante. Comme avec le format à virgule flottante simple précision, il manque de précision sur les nombres entiers par rapport à un format entier de la même taille. Il est communément appelé simplement double. La norme IEEE 754 spécifie un binaire64 comme ayant:
- Bit de signe: 1 bit
- Exposant: 11 bits
- Précision significative: 53 bits (52 explicitement stockés)
La valeur réelle supposée par une donnée double précision 64 bits donnée avec un exposant biaisé donné et une fraction 52 bits est
ou
Merci à @a_guest de me l'avoir signalé.
Une question différente a été nommée en double de celle-ci:
En C ++, pourquoi le résultat est-il cout << x
différent de la valeur affichée par un débogueur x
?
Le x
dans la question est une float
variable.
Un exemple serait
float x = 9.9F;
Le débogueur montre que 9.89999962
la sortie de l' cout
opération est 9.9
.
La réponse s'avère être cout
la précision par défaut de float
6, donc elle arrondit à 6 chiffres décimaux.
Voir ici pour référence