Les mathématiques en virgule flottante sont-elles cassées?

Feb 26 2009

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

2379 DanielScott Feb 26 2009 at 04:40

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 multiplié par une puissance de deux; les nombres rationnels (tels que 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.

Car 0.1dans le binary64format standard , la représentation peut être écrite exactement comme

  • 0.1000000000000000055511151231257827021181583404541015625 en décimal, ou
  • 0x1.999999999999ap-4en notation hexfloat C99 .

En revanche, le nombre rationnel 0.1, qui est 1/10, peut être écrit exactement comme

  • 0.1 en décimal, ou
  • 0x1.99999999999999...p-4dans un analogue de la notation hexfloat C99, où le ...représente une séquence sans fin de 9.

Les constantes 0.2et 0.3dans votre programme seront également des approximations de leurs vraies valeurs. Il arrive que le plus proche doublede 0.2soit plus grand que le nombre rationnel 0.2mais que le plus proche doublede 0.3soit plus petit que le nombre rationnel 0.3. La somme de 0.1et 0.2finit par être plus grande que le nombre rationnel 0.3et donc en désaccord avec la constante de votre code.

Un traitement assez complet des problèmes d'arithmétique à virgule flottante est ce que tout informaticien devrait savoir sur l' arithmétique à virgule flottante . Pour une explication plus facile à digérer, consultez floating-point-gui.de .

Note latérale: Tous les systèmes numériques positionnels (base-N) partagent ce problème avec précision

Les vieux nombres décimaux simples (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 trouve être facile à représenter avec le système décimal, mais qui ne correspond pas au système binaire. Cela va dans les deux sens (dans une certaine mesure) aussi: 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 dans l'habitude d'utiliser un système numérique de base 2 dans notre vie quotidienne, vous regarderiez même ce nombre et vous compreniez instinctivement que vous pourriez y arriver en divisant par deux quelque chose, en le divisant par deux encore et encore et encore.

** Bien sûr, ce n'est pas exactement ainsi que 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 à se produire parce que les nombres du «monde réel» avec lesquels nous sommes généralement intéressés à 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, puisque 5/7 ne peut pas être représenté exactement avec un nombre décimal).

Donc non: les nombres binaires à virgule flottante ne sont pas cassés, ils sont juste aussi imparfaits que tous les autres systèmes de nombres en base N :)

Note latérale: Travailler avec des flotteurs dans la programmation

En 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éressent avant de les afficher.

Vous devez également remplacer les tests d'égalité par des comparaisons qui autorisent une certaine tolérance, ce qui signifie:

Ne fais pasif (x == y) { ... }

Faites plutôt if (abs(x - y) < myToleranceValue) { ... }.

absest la valeur absolue. myToleranceValuedoit être choisi pour votre application particulière - et cela dépendra beaucoup de la "marge de manœuvre" que vous êtes prêt à autoriser et du nombre le plus élevé 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. Celles-ci ne doivent pas être utilisées comme valeurs de tolérance.

625 KernelPanik Apr 18 2013 at 18:52

Le point de vue d'un concepteur de matériel

Je crois que je devrais ajouter la perspective d'un concepteur de matériel à cela puisque je conçois et construis du matériel en virgule flottante. Connaître l'origine de l'erreur peut aider à comprendre ce qui se passe dans le logiciel, et j'espère que cela aidera à expliquer les raisons pour lesquelles les erreurs en virgule flottante se produisent et semblent s'accumuler avec le temps.

1. Vue d'ensemble

Du point de vue de l'ingénierie, la plupart des opérations en virgule flottante auront un élément d'erreur puisque le matériel qui effectue les calculs en virgule flottante ne doit avoir qu'une erreur de moins de la moitié d'une unité en dernier lieu. Par conséquent, une grande partie du matériel s'arrêtera à une précision qui n'est nécessaire que pour générer une erreur inférieure à 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 pris par l'unité. 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'additionnent au fil du temps.

2. Normes

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. Ce qui suit, cependant, couvrira le mode normalisé de IEEE-754 qui est le mode de fonctionnement typique.

Dans la norme IEEE-754, les concepteurs de matériel sont autorisés à utiliser n'importe quelle valeur d'erreur / epsilon tant qu'elle est inférieure à la moitié d'une unité à la dernière place, et que le résultat doit être inférieur à la moitié d'une unité 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, puisque 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 plus en détail les causes des erreurs matérielles sur diverses opérations en virgule flottante.

3. Cause de l'erreur d'arrondi dans la division

La principale cause de l'erreur de 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 l'inverse, principalement 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 tout ce qui a une erreur de moins d'une unité en dernier lieu. La table des réciproques 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 de la base, 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 de la base du diviseur, plus quelques bits de garde k, où k>=2. Ainsi, par exemple, une table de sélection de quotient typique pour un diviseur qui calcule 2 bits du quotient à la fois (base 4) serait des 2+2= 4bits (plus quelques bits optionnels).

3.1 Erreur d'arrondi de division: approximation de la réciproque

Les réciproques 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 produire l'erreur la plus faible possible. Dans tous les cas, cependant, toutes les réciproques sont des approximations de la réciproque réelle et introduisent 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. unité à la dernière place. Les méthodes de division lente calculent un nombre fixe de chiffres du quotient dans 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.

4. Erreurs d'arrondi dans d'autres opérations: troncature

Les différents modes de troncature de la réponse finale autorisés par IEEE-754 sont une autre cause des erreurs d'arrondi dans toutes les opérations. Il y a tronqué, arrondi vers zéro, arrondi au plus proche (par défaut), arrondi vers le bas et arrondi vers le haut. Toutes les méthodes introduisent un élément d'erreur de moins d'une unité en dernier lieu pour une seule opération. Au fil du temps et des opérations répétées, la troncature ajoute également de manière cumulative à l'erreur résultante. Cette erreur de troncature est particulièrement problématique en exponentiation, qui implique une certaine forme de multiplication répétée.

5. Opérations répétées

Étant donné que le matériel qui effectue les calculs en virgule flottante doit uniquement 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, avec le temps, les erreurs sont plus susceptibles de s'annuler. out, et Arithmétique d'intervalle combinée avec des variations des modes d'arrondi IEEE 754 pour prédire les erreurs d'arrondi et les corriger. En raison de sa faible erreur relative par rapport aux autres modes d'arrondi, arrondir 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 en dernier lieu , garantit une erreur inférieure à la moitié d'une unité en dernier lieu pour une opération. L'utilisation de la troncature, de l'arrondissement et de l'arrondi vers le bas 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, de sorte que ces modes ne sont pas recommandés sauf s'ils sont utilisé en arithmétique d'intervalle.

6. Résumé

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 en cas de division. Étant donné que la norme IEEE-754 ne nécessite qu'une erreur inférieure à la moitié d'une unité en dernier lieu pour une seule opération, les erreurs en virgule flottante sur des opérations répétées s'additionneront à moins qu'elles ne soient corrigées.

481 JoelCoehoorn Feb 26 2009 at 04:43

Il est cassé de la même manière que la notation décimale (base-10) est cassée, juste pour la base-2.

Pour comprendre, pensez à représenter 1/3 sous forme de valeur décimale. Il est impossible de faire exactement! De la même manière, 1/10 (décimal 0,1) ne peut pas être représenté exactement en base 2 (binaire) comme une valeur "décimale"; un motif répétitif après la virgule décimale se poursuit indéfiniment. La valeur n'est pas exacte et vous ne pouvez donc pas faire de calculs exacts avec elle en utilisant les méthodes normales en virgule flottante.

322 ChrisJester-Young Nov 20 2014 at 09:39

La plupart des réponses ici abordent cette question en termes techniques très secs. Je voudrais aborder cela en des termes que les êtres humains normaux peuvent comprendre.

Imaginez que vous essayez de découper des pizzas. Vous disposez d'un coupe-pizza robotisé qui peut couper les tranches de pizza exactement en deux. Il peut réduire de moitié une pizza entière, ou il peut réduire de moitié 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, coupez-la de moitié et continuez à réduire de moitié la plus petite tranche à chaque fois, vous pouvez réduire de moitié 53 fois avant que la tranche ne soit trop petite pour même ses capacités de haute précision. . À ce stade, vous ne pouvez plus réduire de moitié cette tranche très fine, mais vous devez l'inclure ou l'exclure telle quelle.

Maintenant, comment découperiez-vous toutes les tranches de manière à ce qu'elles totalisent 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, bien sûr, connaissent 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 que vous les coupez. Vous pouvez faire une assez bonne approximation, et si vous additionnez l'approximation de 0,1 avec l'approximation de 0,2, vous obtenez une assez bonne approximation de 0,3, mais ce n'est toujours que cela, une approximation.

Pour les nombres à double précision (qui est la précision qui vous permet de réduire de moitié votre pizza 53 fois), les nombres immédiatement inférieurs et supérieurs à 0,1 sont 0,09999999999999999167332731531132594682276248931884765625 et 0.1000000000000000055511151231257827021181583404541015625. Ce dernier est un peu plus proche de 0,1 que le premier, donc un analyseur numérique, étant donné une entrée de 0,1, favorisera ce dernier.

(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 nombres 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 pousseront 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


PS Certains langages de programmation fournissent également des coupeurs de pizza qui peuvent diviser des tranches en dixièmes exacts . Bien que ces coupeurs de pizza soient rares, si vous en avez accès, vous devriez l'utiliser lorsqu'il est important de pouvoir obtenir exactement un dixième ou un cinquième d'une tranche.

(Publié à l'origine sur Quora.)

215 DevinJeanpierre Feb 26 2009 at 04:41

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 vaut «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.

126 DanielVassallo Apr 09 2010 at 19:25

En plus des autres réponses correctes, 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.3retourne falseen 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 par la mise à 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 traiter l'argent comme un entier représentant le nombre de cents: 2550cents au lieu de 25.50dollars.


1 Douglas Crockford: JavaScript: Les bonnes parties : Annexe A - Les horribles parties (page 105) .

120 WaiHaLee Feb 24 2015 at 00:15

Ma réponse est assez longue, je l'ai donc divisée en trois sections. Puisque la question porte sur les mathématiques en 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 binaire à virgule flottante double précision IEEE 754 (binaire64) représente un nombre de la forme

valeur = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

en 64 bits:

  • Le premier bit est le bit de signe : 1si le nombre est négatif, 0sinon 1 .
  • Les 11 bits suivants sont l' exposant , qui est décalé de 1023. En d'autres termes, après lecture des bits d'exposant à partir d'un nombre à double précision, 1023 doivent être soustraits pour obtenir la puissance de deux.
  • Les 52 bits restants sont le significande (ou mantisse). Dans la mantisse, un «implicite» 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é - +0et -0sont traités différemment: 1 / (+0)est l'infini positif; 1 / (-0)est l'infini négatif. Pour les valeurs nulles, la mantisse et les bits 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 offset de zéro (et un implicite 0.). La plage des nombres à double précision dénormaux 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 constituée de 1s) est 2-1023 + 1 - deux -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 au but: la question initiale

(Aller en bas pour la version TL; DR)

Cato Johnston (le demandeur) a demandé pourquoi 0,1 + 0,2! = 0,3.

Écrites 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. C'est la clé de la raison pour laquelle il y a une erreur dans les calculs - 0,1, 0,2 et 0,3 ne peuvent pas être représentés précisément en binaire dans un nombre fini de bits binaires, pas plus de 1/9, 1/3 ou 1/7 peuvent être représentés précisément en chiffres décimaux .

Notez également que nous pouvons diminuer la puissance de l'exposant de 52 et décaler le point de la représentation binaire vers la droite de 52 places (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 du décalage et la réajout des 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, aboutit à une valeur qui ne diffère que dans le bit le moins significatif.

TL; DR

En écrivant 0.1 + 0.2dans une représentation binaire IEEE 754 (avec des deux-points séparant les trois parties) et en la comparant à 0.3, voici (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 de 2 à 54 , soit ~ 5,5511151231258 × 10 -17 - insignifiante (pour de nombreuses applications) par rapport aux valeurs d'origine.

Comparer les derniers bits d'un nombre à virgule flottante est intrinsèquement dangereux, 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).

La plupart des calculatrices utilisent des chiffres de garde supplémentaires pour contourner ce problème, ce 0.1 + 0.2qui donnerait 0.3: les derniers bits sont arrondis.

59 MarkRansom Mar 16 2016 at 12:27

Les nombres à virgule flottante stockés dans l'ordinateur se composent de deux parties, un entier et un exposant que la base est prise et multipliée par la partie entière.

Si l'ordinateur fonctionnait en base 10, il 0.1serait 1 x 10⁻¹, 0.2serait 2 x 10⁻¹et 0.3serait 3 x 10⁻¹. Le calcul des nombres entiers est facile et exact, donc l'ajout 0.1 + 0.2se 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.5is 1 x 2⁻¹et 0.25is 1 x 2⁻², et leur ajout entraîne 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.1est 3602879701896397 x 2⁻⁵⁵et le nombre le plus proche de 0.2est 7205759403792794 x 2⁻⁵⁵; en les ajoutant ensemble, on obtient 10808639105689191 x 2⁻⁵⁵une valeur décimale exacte de 0.3000000000000000444089209850062616169452667236328125. Les nombres à virgule flottante sont généralement arrondis pour l'affichage.

49 BrettDaniel Feb 26 2009 at 04:42

Erreur d'arrondi à virgule flottante. D'après ce que tout informaticien doit 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 produiront des quantités qui ne peuvent pas être exactement représentées en utilisant autant de bits. Par conséquent, le résultat d'un calcul en virgule flottante doit souvent être arrondi afin de rentrer dans sa représentation finie. Cette erreur d'arrondi est la caractéristique du calcul en virgule flottante.

33 Justineo Dec 26 2011 at 13:51

Ma solution de contournement:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

La précision fait référence au nombre de chiffres que vous souhaitez conserver après la virgule décimale lors de l'addition.

30 bruziuz Oct 06 2014 at 01:39

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 à virgule flottante IEEE754.

Le modèle pour stocker les nombres réels sous le capot représente les nombres flottants comme

Même si vous pouvez taper 0.2facilement, FLT_RADIXet DBL_RADIXvaut 2; pas 10 pour un ordinateur avec FPU qui utilise la "norme IEEE pour l'arithmétique binaire à virgule flottante (ISO / IEEE Std 754-1985)".

Il est donc un peu difficile de représenter exactement ces chiffres. Même si vous spécifiez cette variable explicitement sans aucun calcul intermédiaire.

29 KostasChalkias Jan 03 2015 at 19:12

Quelques statistiques liées à cette fameuse question à double précision.

Lorsque vous ajoutez toutes les valeurs ( a + b ) en utilisant un pas de 0,1 (de 0,1 à 100), nous avons ~ 15% de chances 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 - ba> 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, alors utilisez toujours BigDecimal lorsque la précision est d'une grande importance. Avec 2 chiffres décimaux (pas 0,01), la situation empire un peu plus (18% et 36%).

28 DigitalRoss Feb 03 2016 at 06:49

Non, pas cassé, mais la plupart des fractions décimales doivent être approximées

Résumé

L'arithmétique à 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 aviez essayé cela en utilisant FP, votre 0,01 aurait été légèrement décalé, donc la seule façon d'en ajouter 25 jusqu'à un joli 0,25 exact aurait nécessité une longue chaîne de causalité impliquant des bits de garde et des arrondis. C'est difficile à prévoir, alors nous lançons nos mains et disons "FP est 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écimal de fin) est un nombre rationnel de la forme

           a / (2 n x 5 m )

En binaire, on n'obtient que le terme 2 n , c'est-à-dire:

           a / 2 n

Donc , en décimal, on ne peut pas représenter 1 / 3 . Étant donné que la base 10 inclut 2 comme facteur premier, chaque nombre que nous pouvons écrire sous forme de fraction binaire peut également être écrit sous forme de fraction de base 10. Cependant, presque rien de ce que nous écrivons sous forme de 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 des 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, bien que chaque fraction binaire puisse ê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.

Traiter avec ça

Les développeurs sont généralement invités à faire des comparaisons <epsilon , un meilleur conseil pourrait être d'arrondir à des valeurs intégrales (dans la bibliothèque C: round () et roundf (), c'est-à-dire de rester au format FP), puis de comparer. Arrondir à une longueur de fraction décimale spécifique résout la plupart des problèmes de sortie.

De plus, sur les problèmes réels de calcul des nombres (les problèmes pour lesquels FP a été inventé sur des ordinateurs précoces et terriblement coûteux), 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 de toute façon "inexacte". La "précision" 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 FP pour le comptage des haricots. Cela fonctionne pour cela, mais seulement si vous vous en tenez à des valeurs intégrales, ce qui va à l'encontre de l'intérêt de l'utiliser. C'est pourquoi nous avons toutes ces bibliothèques de logiciels de fraction décimale.

J'adore la réponse Pizza de Chris , car elle décrit le problème réel, et pas seulement les agitations habituelles sur «l'inexactitude». Si la PF était simplement «inexacte», nous pourrions corriger cela et nous l'aurions fait il y a des décennies. La raison pour laquelle nous ne l'avons pas fait est que le format FP est compact et rapide et c'est le meilleur moyen de traiter beaucoup de chiffres. En outre, c'est un héritage de l'ère spatiale et de la course aux armements et des premières tentatives de résolution 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 ne comptez que des beans dans une banque, les solutions logicielles qui utilisent en premier lieu des représentations de chaînes décimales fonctionnent parfaitement bien. Mais vous ne pouvez pas faire de la chromodynamique quantique ou de l'aérodynamique de cette façon.

26 MuhammadMusavi Aug 07 2018 at 16:34

Bref c'est parce que:

Les nombres à virgule flottante ne peuvent pas représenter toutes les décimales avec précision en binaire

Donc, tout comme 3/10 qui n'existe pas en base 10 précisément (ce sera 3,33 ... récurrent), de la même manière 1/10 n'existe pas en binaire.

Et alors? Comment y faire face? Y a-t-il une solution de contournement?

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

Laissez-moi vous 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.2et 0.7voici: 0.2 + 0.7 = 0.8999999999999999.

Le résultat attendu était que 0.9cela signifie que vous avez besoin d'un résultat avec une précision de 1 chiffre dans ce cas. Donc vous devriez 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), alors quel devrait être le paramètre pour s'adapter à chaque nombre flottant donné?

Vous pourriez dire que ce soit 10 dans chaque situation alors:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Mince! Qu'allez-vous faire avec ces zéros indésirables après 9? Il est temps de le convertir en flottant 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, mieux vaut la proposer sous la forme d'une fonction comme celle-ci:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }
    

Essayons-le 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 le suggère W3SCHOOLS, 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 / 10cela ne fonctionnera pas du tout, même si cela semble identique! Je préfère la première solution car je peux l'appliquer en tant que fonction qui convertit le flotteur d'entrée en flotteur de sortie précis.

18 workoverflow Aug 01 2012 at 14:02

Avez-vous essayé la solution de ruban adhésif?

Essayez de déterminer quand des erreurs se produisent et de les corriger avec de courtes instructions if, 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, cela va se transformer en gros dragon et vous mordre dans le **

16 PiyushS528 Oct 14 2013 at 23:45

Ces nombres étranges apparaissent parce que les ordinateurs utilisent un système de nombres binaires (base 2) à des fins de calcul, alors que nous utilisons des nombres décimaux (base 10).

Il existe une majorité de nombres fractionnaires qui ne peuvent être représentés avec précision ni en binaire, ni en décimal, ni les deux. Résultat - Un nombre arrondi (mais précis) résulte.

16 AndreaCorbellini Aug 21 2015 at 21:53

Étant donné que personne n'a mentionné cela ...

Certains langages de haut niveau tels que Python et Java sont fournis avec des outils permettant de surmonter les limitations de la virgule flottante binaire. Par exemple:

  • decimalModule Python et BigDecimalclasse Java , qui représentent les nombres en interne avec la 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 les problèmes les plus courants avec l'arithmétique binaire à virgule flottante.

    Les décimales sont très bien lorsqu'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 decimalmodule de Python est basé sur la norme IEEE 854-1987 .

  • Le fractionsmodule Python et la BigFractionclasse 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 exigeons une très grande précision), mais elles résolvent tout de même un grand nombre de problèmes d'arithmétique binaire à virgule flottante.

16 PatriciaShanahan Dec 21 2015 at 18:15

Bon nombre des nombreux doublons de cette question portent sur les effets de l'arrondi en 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 intéressants plutôt qu'en lisant simplement à ce sujet. Certains langages offrent des moyens de le faire, comme la conversion d'un floatou doublevers BigDecimalen 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 se convertit en 0.1000000000000000055511151231257827021181583404541015625,

0,2 se convertit en 0.200000000000000011102230246251565404236316680908203125,

0,3 se convertit en 0,299999999999999988897769753748434595763683319091796875, 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 0,3000000000000000166533453693773481063544750213623046875.

S'il était arrondi à l'équivalent de 0,3, l'erreur d'arrondi serait de 0,000000000000000000277555756156289135105907917022705078125. Arrondir à l'équivalent de 0,30000000000000004 donne également une erreur d'arrondi 0,00000000000000277555756156289135105907917022705078125. Le bris d'égalité rond-à-pair 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.

15 Noname Mar 18 2016 at 07:38

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)=truemoins d'avoir l'infini pour ajouter 0,333 ... à 0,333 ... donc tout comme avec le (1/10+2/10)!==3/10problè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é ...

9 BlairHoughton Oct 05 2015 at 22:55

Le type de calcul en 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 eux. (La version standard compte plus de cinquante pages de documentation et dispose d'un comité pour traiter ses errata et affiner davantage.)

Cette approximation est un mélange d'approximations de différents types, dont chacune peut être soit ignorée, soit soigneusement prise 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, tant au niveau matériel que logiciel, que la plupart des gens passent juste 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 devriez é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 en virgule flottante sont floues en valeur et en logique et que 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 qu'il y a dans votre FPU.

9 alinsoar Dec 29 2016 at 17:29

Juste pour m'amuser, j'ai joué avec la représentation des flotteurs, en suivant les définitions du Standard 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, affichera 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 xxtelle sorte que la somme imprimée par la fonction yysoit é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 binaire du flottant le nombre 1,000,000,000

Après le code, j'attache une session de console, dans laquelle je calcule la somme des termes pour les deux constantes (moins PI et 999999999) qui existe vraiment dans le matériel, insérée 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 de console dans laquelle je calcule la valeur réelle du flottant qui existe dans le matériel. J'avais l'habitude bcd'imprimer la somme des termes émis par le programme principal. On peut insérer cette somme en python replou quelque chose de similaire également.

-- .../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 tout. La valeur de 999999999 est en fait

999999999.999999446351872

Vous pouvez également vérifier bcque -3.14 est également perturbé. N'oubliez pas de définir un scalefacteur bc.

La somme affichée est ce qui se trouve à 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 scalefacteur à 15. Mathématiquement, avec une précision infinie, il semble qu'il soit de 1 000 000 000.

5 TorstenBecker Dec 20 2017 at 05:37

Une autre façon de voir cela: 64 bits sont utilisés pour représenter des nombres. En conséquence, il est impossible de représenter plus de 2 ** 64 = 18 446 744 073 709 551 616 nombres différents.

Cependant, Math dit qu'il y a 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, il y a donc des espaces entre les nombres représentés avec précision et remplis de chiffres seulement approximatifs.

Malheureusement, 0,3 se situe dans une lacune.

5 nauer Aug 08 2018 at 15:47

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
4 DanielMcLaury Dec 21 2018 at 01:27

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

Tronquant à huit décimales, on obtient

0.33333333 + 0.66666666 = 0.99999999

ce qui est, bien sûr, différent de 1.00000000exactement 0.00000001.


La situation pour les nombres binaires avec un nombre fixe de bits est exactement analogue. En tant que nombres réels, nous avons

1/10 = 0,0001100110011001100 ... (base 2)

et

1/5 = 0,0011001100110011001 ... (base 2)

Si nous les tronquions à, disons, sept bits, alors nous obtiendrions

0.0001100 + 0.0011001 = 0.0100101

tandis que d'un autre côté,

3/10 = 0,01001100110011 ... (base 2)

qui, tronqué à 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.0001100nous 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.

4 chqrlie Apr 22 2019 at 08:02

Les nombres décimaux tels que 0.1, 0.2et 0.3ne sont pas représentés exactement dans les types à virgule flottante codés en binaire. La somme des approximations pour 0.1et 0.2diffère de l'approximation utilisée pour 0.3, d'où le mensonge de 0.1 + 0.2 == 0.3comme on peut le voir 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 ces types par défaut, mais comme une extension décrite dans un rapport technique .

Les types _Decimal32, _Decimal64et _Decimal128peuvent être disponibles sur votre système (par exemple, GCC les prend en charge sur les cibles sélectionnées , mais Clang ne les prend pas en charge sous OS X ).

3 Piedone Dec 22 2017 at 23:39

Étant donné que ce fil s'est un peu ramifié 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.

Jeter un coup d'œ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 je comprends bien, cela résout également le type de problèmes dans la question. Projet assez intéressant, la personne derrière est un mathématicien du 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).

3 VladAgurets May 08 2019 at 02:45

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. Donc 1/2, 1/4, 1/5, 1/8 et 1/10 peuvent tous être exprimés proprement parce que 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 parce que 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 que des fractions proprement qui contiennent seulement 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éterait des décimales. Ainsi, 0,1 et 0,2 (1/10 et 1/5), alors que des décimales nettes dans un système de base 10, répètent des décimales dans le système de base 2 dans lequel l'ordinateur fonctionne. Lorsque vous faites des calculs sur ces décimales répétitives, vous vous retrouvez avec des restes qui sont reportés lorsque vous convertissez le nombre de base 2 (binaire) de l'ordinateur en un nombre de base 10 plus lisible par l'homme.

De https://0.30000000000000004.com/

2 RollerSimmer Aug 20 2020 at 22:38

L'arithmétique normale est la base 10, donc les décimales représentent les dixièmes, les centièmes, etc. Lorsque vous essayez de représenter un nombre à virgule flottante en arithmétique binaire en base 2, vous avez affaire à des moitiés, des quarts, des huitièmes, etc.

Dans le matériel, les virgules flottantes sont stockées sous forme de mantisses et d'exposants entiers. La mantisse représente les chiffres significatifs. L'exposant est comme la notation scientifique mais il utilise une base de 2 au lieu de 10. Par exemple, 64,0 serait représenté avec une mantisse de 1 et un exposant de 6. 0,125 serait représenté avec une mantisse de 1 et un exposant de -3.

Les décimales à virgule flottante doivent additionner des puissances négatives de 2

0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d

etc.

Il est courant d'utiliser un delta d'erreur au lieu d'utiliser des opérateurs d'égalité lorsqu'il s'agit d'arithmétique à virgule flottante. Au lieu de

if(a==b) ...

vous utiliseriez

delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
1 TheMachinist Aug 03 2020 at 22:03

Les nombres à virgule flottante sont représentés, au niveau matériel, sous forme de fractions de nombres binaires (base 2). Par exemple, la fraction décimale:

0.125

a la valeur 1/10 + 2/100 + 5/1000 et, de la même manière, la fraction binaire:

0.001

a la valeur 0/2 + 0/4 + 1/8. Ces deux fractions ont la même valeur, la seule différence est que la première est une fraction décimale, la seconde est une fraction binaire.

Malheureusement, la plupart des fractions décimales ne peuvent pas avoir de représentation exacte en fractions binaires. Par conséquent, en général, les nombres à virgule flottante que vous donnez ne sont approximés qu'en fractions binaires à stocker dans la machine.

Le problème est plus facile à aborder en base 10. Prenons par exemple la fraction 1/3. Vous pouvez l'approcher d'une fraction décimale:

0.3

ou mieux,

0.33

ou mieux,

0.333

etc. Peu importe le nombre de décimales que vous écrivez, le résultat n'est jamais exactement 1/3, mais c'est une estimation qui se rapproche toujours.

De même, quel que soit le nombre de décimales de base 2 que vous utilisez, la valeur décimale 0,1 ne peut pas être représentée exactement comme une fraction binaire. En base 2, 1/10 est le nombre périodique suivant:

0.0001100110011001100110011001100110011001100110011 ...

Arrêtez-vous à toute quantité finie de bits et vous obtiendrez une approximation.

Pour Python, sur une machine typique, 53 bits sont utilisés pour la précision d'un flottant, donc la valeur stockée lorsque vous entrez la décimale 0,1 est la fraction binaire.

0.00011001100110011001100110011001100110011001100110011010

ce qui est proche, mais pas exactement égal, de 1/10.

Il est facile d'oublier que la valeur stockée est une approximation de la fraction décimale d'origine, en raison de la façon dont les flottants sont affichés dans l'interpréteur. Python n'affiche qu'une approximation décimale de la valeur stockée en binaire. Si Python devait afficher la vraie valeur décimale de l'approximation binaire stockée pour 0,1, il afficherait:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Il s'agit de beaucoup plus de décimales que la plupart des gens ne s'y attendraient, donc Python affiche une valeur arrondie pour améliorer la lisibilité:

>>> 0.1
0.1

Il est important de comprendre qu'en réalité c'est une illusion: la valeur stockée n'est pas exactement 1/10, c'est simplement sur l'affichage que la valeur stockée est arrondie. Cela devient évident dès que vous effectuez des opérations arithmétiques avec ces valeurs:

>>> 0.1 + 0.2
0.30000000000000004

Ce comportement est inhérent à la nature même de la représentation en virgule flottante de la machine: ce n'est pas un bogue en Python, ni un bogue dans votre code. Vous pouvez observer le même type de comportement dans toutes les autres langues qui utilisent la prise en charge matérielle pour le calcul des nombres à virgule flottante (bien que certaines langues ne rendent pas la différence visible par défaut, ou pas dans tous les modes d'affichage).

Une autre surprise est inhérente à celle-ci. Par exemple, si vous essayez d'arrondir la valeur 2,675 à deux décimales, vous obtiendrez

>>> round (2.675, 2)
2.67

La documentation de la primitive round () indique qu'elle arrondit à la valeur la plus proche de zéro. Puisque la fraction décimale est exactement à mi-chemin entre 2,67 et 2,68, vous devriez vous attendre à obtenir (une approximation binaire de) 2,68. Ce n'est cependant pas le cas, car lorsque la fraction décimale 2,675 est convertie en flottant, elle est stockée par une approximation dont la valeur exacte est:

2.67499999999999982236431605997495353221893310546875

Comme l'approximation est légèrement plus proche de 2,67 que de 2,68, l'arrondi est à la baisse.

Si vous êtes dans une situation où l'arrondissement des nombres décimaux à mi-chemin est important, vous devez utiliser le module décimal. En passant, le module décimal fournit également un moyen pratique de "voir" la valeur exacte stockée pour tout flottant.

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

Une autre conséquence du fait que 0,1 n'est pas exactement stocké dans 1/10 est que la somme de dix valeurs de 0,1 ne donne pas non plus 1,0:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

L'arithmétique des nombres binaires à virgule flottante réserve de nombreuses surprises. Le problème avec "0,1" est expliqué en détail ci-dessous, dans la section "Erreurs de représentation". Voir Les dangers de la virgule flottante pour une liste plus complète de ces surprises.

Il est vrai qu'il n'y a pas de réponse simple, cependant ne soyez pas trop méfiant envers les nombres virtula flottants! Les erreurs, en Python, dans les opérations sur les nombres à virgule flottante sont dues au matériel sous-jacent, et sur la plupart des machines, elles ne dépassent pas 1 sur 2 ** 53 par opération. C'est plus que nécessaire pour la plupart des tâches, mais vous devez garder à l'esprit qu'il ne s'agit pas d'opérations décimales et que chaque opération sur des nombres à virgule flottante peut souffrir d'une nouvelle erreur.

Bien que des cas pathologiques existent, pour la plupart des cas d'utilisation courants, vous obtiendrez le résultat attendu à la fin en arrondissant simplement au nombre de décimales que vous souhaitez sur l'affichage. Pour un contrôle précis de l'affichage des flottants, consultez Syntaxe de mise en forme des chaînes pour les spécifications de mise en forme de la méthode str.format ().

Cette partie de la réponse explique en détail l'exemple de "0,1" et montre comment vous pouvez effectuer une analyse exacte de ce type de cas par vous-même. Nous supposons que vous êtes familier avec la représentation binaire des nombres à virgule flottante. Le terme Erreur de représentation signifie que la plupart des fractions décimales ne peuvent pas être représentées exactement en binaire. C'est la raison principale pour laquelle Python (ou Perl, C, C ++, Java, Fortran et bien d'autres) n'affiche généralement pas le résultat exact en décimal:

>>> 0.1 + 0.2
0.30000000000000004

Pourquoi ? 1/10 et 2/10 ne sont pas représentables exactement en fractions binaires. Cependant, toutes les machines d'aujourd'hui (juillet 2010) suivent la norme IEEE-754 pour l'arithmétique des nombres à virgule flottante. et la plupart des plates-formes utilisent une «double précision IEEE-754» pour représenter les flottants Python. La double précision IEEE-754 utilise 53 bits de précision, donc en lisant l'ordinateur essaie de convertir 0,1 à la fraction la plus proche de la forme J / 2 ** N avec J un entier d'exactement 53 bits. Réécrire:

1/10 ~ = J / (2 ** N)

dans :

J ~ = 2 ** N / 10

en se rappelant que J est exactement 53 bits (donc> = 2 ** 52 mais <2 ** 53), la meilleure valeur possible pour N est 56:

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

Donc 56 est la seule valeur possible pour N qui laisse exactement 53 bits pour J. La meilleure valeur possible pour J est donc ce quotient, arrondi:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

Puisque le report est supérieur à la moitié de 10, la meilleure approximation est obtenue en arrondissant:

>>> q + 1
7205759403792794

Par conséquent, la meilleure approximation possible pour 1/10 en «double précision IEEE-754» est celle-ci au-dessus de 2 ** 56, c'est-à-dire:

7205759403792794/72057594037927936

Notez que puisque l'arrondi a été fait à la hausse, le résultat est en fait légèrement supérieur à 1/10; si nous n'avions pas arrondi, le quotient aurait été légèrement inférieur à 1/10. Mais en aucun cas c'est exactement 1/10!

Ainsi, l'ordinateur ne "voit" jamais 1/10: ce qu'il voit est la fraction exacte donnée ci-dessus, la meilleure approximation utilisant les nombres à virgule flottante double précision du "" IEEE-754 ":

>>>. 1 * 2 ** 56
7205759403792794.0

Si nous multiplions cette fraction par 10 ** 30, nous pouvons observer les valeurs de ses 30 décimales de poids fort.

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

ce qui signifie que la valeur exacte stockée dans l'ordinateur est approximativement égale à la valeur décimale 0.100000000000000005551115123125. Dans les versions antérieures à Python 2.7 et Python 3.1, Python arrondissait ces valeurs à 17 décimales significatives, affichant «0.10000000000000001». Dans les versions actuelles de Python, la valeur affichée est la valeur dont la fraction est la plus courte possible tout en donnant exactement la même représentation lorsqu'elle est reconvertie en binaire, affichant simplement «0,1».

costargc Oct 06 2019 at 04:46

Je viens de voir ce problème intéressant autour des points flottants:

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: binary64

À 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 plage plus large par rapport à la virgule flottante simple précision, malgré ses performances et son coût en 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 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 stockés explicitement)

La valeur réelle prise par une donnée double précision de 64 bits avec un exposant biaisé donné et une fraction de 52 bits est

ou

Merci à @a_guest de me l'avoir signalé.