Est-il possible d’obtenir 0 en soustrayant deux nombres à virgule flottante inégale?

Est-il possible d’obtenir une division par 0 (ou infini) dans l’exemple suivant?

public double calculation(double a, double b) { if (a == b) { return 0; } else { return 2 / (a - b); } } 

Dans des cas normaux, ce ne sera évidemment pas le cas. Mais si a et b sont très proches, (ab) peut (ab) à 0 raison de la précision du calcul?

Notez que cette question concerne Java, mais je pense que cela s’appliquera à la plupart des langages de programmation.

En Java, a - b n’est jamais égal à 0 si a != b Ceci est dû au fait que Java impose les opérations IEEE 754 en virgule flottante qui prennent en charge les nombres dénormalisés. De la spécification :

En particulier, le langage de programmation Java nécessite la prise en charge de nombres à virgule flottante dénormalisés IEEE 754 et d’un débordement progressif, ce qui facilite la démonstration des propriétés souhaitables d’algorithmes numériques particuliers. Les opérations à virgule flottante ne sont pas “à zéro” si le résultat calculé est un nombre dénormalisé.

Si une FPU fonctionne avec des nombres dénormalisés , la soustraction des nombres inégaux ne peut jamais produire zéro (contrairement à la multiplication), voir aussi cette question .

Pour les autres langues, cela dépend. En C ou C ++, par exemple, le support IEEE 754 est facultatif.

Cela dit, il est possible que l’expression 2 / (a - b) déborde, par exemple avec a = 5e-308 et b = 4e-308 .

En guise de solution, qu’en est-il des éléments suivants?

 public double calculation(double a, double b) { double c = a - b; if (c == 0) { return 0; } else { return 2 / c; } } 

De cette façon, vous ne dépendez pas du support IEEE dans aucune langue.

Vous n’obtiendrez pas de division par zéro quelle que soit la valeur de a - b , car la division en virgule flottante par 0 ne lance pas d’exception. Il retourne l’infini.

Maintenant, la seule façon pour a == b retourner true est si a et b contiennent exactement les mêmes bits. S’ils diffèrent par le bit le moins significatif, la différence entre eux ne sera pas 0.

MODIFIER :

Comme Bathsheba l’a correctement commenté, il y a quelques exceptions:

  1. “Pas un nombre compare” faux avec lui-même mais aura des modèles de bits identiques.

  2. -0.0 est défini pour être comparé à +0.0, et leurs modèles de bits sont différents.

Donc, si a et b sont tous deux Double.NaN , vous atteindrez la clause else, mais comme NaN - NaN renvoie également NaN , vous ne diviserez pas par zéro.

Il n’y a pas de cas où une division par zéro peut se produire ici.

Le SMT Solver Z3 prend en charge l’arithmétique à virgule flottante IEEE précise. Demandons à Z3 de trouver des nombres a et b tels que a != b && (a - b) == 0 :

 (set-info :status unknown) (set-logic QF_FP) (declare-fun b () (FloatingPoint 8 24)) (declare-fun a () (FloatingPoint 8 24)) (declare-fun rm () RoundingMode) (assert (and (not (fp.eq ab)) (fp.eq (fp.sub rm ab) +zero) true)) (check-sat) 

Le résultat est UNSAT . Il n’y a pas de tels chiffres.

La chaîne SMTLIB ci-dessus permet également à Z3 de choisir un mode d’arrondi arbitraire ( rm ). Cela signifie que le résultat est valable pour tous les modes d’arrondi possibles (il y en a cinq). Le résultat inclut également la possibilité que l’une des variables en jeu soit NaN ou l’infini.

a == b est implémenté en qualité fp.eq pour que +0f et -0f comparent. La comparaison avec zéro est également implémentée avec fp.eq Étant donné que la question vise à éviter une division par zéro, c’est la comparaison appropriée.

Si le test d’égalité était implémenté en utilisant l’égalité des bits, +0f et -0f auraient été un moyen de rendre a - b nul. Une version antérieure incorrecte de cette réponse contient des détails sur le mode concernant cette affaire pour les curieux.

Z3 Online ne supporte pas encore la théorie de FPA. Ce résultat a été obtenu en utilisant la dernière twig instable. Il peut être reproduit en utilisant les liaisons .NET comme suit:

 var fpSort = context.MkFPSort32(); var aExpr = (FPExpr)context.MkConst("a", fpSort); var bExpr = (FPExpr)context.MkConst("b", fpSort); var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort()); var fpZero = context.MkFP(0f, fpSort); var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr); var constraintExpr = context.MkAnd( context.MkNot(context.MkFPEq(aExpr, bExpr)), context.MkFPEq(subExpr, fpZero), context.MkTrue() ); var smtlibSsortingng = context.BenchmarkToSMTSsortingng(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr); var solver = context.MkSimpleSolver(); solver.Assert(constraintExpr); var status = solver.Check(); Console.WriteLine(status); 

Utiliser Z3 pour répondre aux questions flottantes IEEE est intéressant car il est difficile de négliger des cas (tels que NaN , -0f , +-inf ) et vous pouvez poser des questions arbitraires. Pas besoin d’interpréter et de citer des spécifications. Vous pouvez même poser des questions mixtes et flottantes telles que “cet algorithme int log2(float) correct?”.

La fonction fournie peut en effet retourner l’infini:

 public class Test { public static double calculation(double a, double b) { if (a == b) { return 0; } else { return 2 / (a - b); } } /** * @param args */ public static void main(Ssortingng[] args) { double d1 = Double.MIN_VALUE; double d2 = 2.0 * Double.MIN_VALUE; System.out.println("Result: " + calculation(d1, d2)); } } 

La sortie est Result: -Infinity .

Lorsque le résultat de la division est trop grand pour être stocké dans un double, l’infini est renvoyé même si le dénominateur est non nul.

Dans une implémentation à virgule flottante conforme à IEEE-754, chaque type à virgule flottante peut contenir des nombres sous deux formats. L’un (“normalisé”) est utilisé pour la plupart des valeurs à virgule flottante, mais le deuxième plus petit nombre qu’il peut représenter n’est qu’un tout petit peu plus grand que le plus petit, et la différence entre eux n’est donc pas représentable dans ce même format. L’autre format (“dénormalisé”) est utilisé uniquement pour les très petits nombres qui ne sont pas représentables dans le premier format.

Les circuits pour gérer efficacement le format à virgule flottante dénormalisé sont coûteux, et tous les processeurs ne l’incluent pas. Certains processeurs offrent le choix entre des opérations sur de très petits nombres beaucoup plus lentes que des opérations sur d’autres valeurs, ou le processeur considère simplement des nombres trop petits pour un format normalisé comme étant égaux à zéro.

Les spécifications Java impliquent que les implémentations doivent prendre en charge le format dénormalisé, même sur les machines où cela entraînerait un ralentissement du code. D’un autre côté, il est possible que certaines implémentations offrent des options pour permettre au code de s’exécuter plus rapidement en échange d’une gestion légèrement négligée des valeurs qui, dans la plupart des cas, serait trop petite (dans les cas où les valeurs sont trop petites peut être ennuyeux d’avoir des calculs avec eux qui prennent dix fois plus de temps que les calculs importants, de sorte que, dans de nombreuses situations pratiques, le zéro à zéro est plus utile que l’arithmétique lente mais précise).

Autrefois, avant IEEE 754, il était fort possible qu’un! = B n’implique pas ab! = 0 et inversement. C’était l’une des raisons de créer IEEE 754 en premier lieu.

Avec IEEE 754, il est presque garanti. Les compilateurs C ou C ++ sont autorisés à effectuer une opération avec une précision supérieure à celle requirejse. Donc, si a et b ne sont pas des variables mais des expressions, alors (a + b)! = C n’implique pas (a + b) – c! = 0, car a + b pourrait être calculé une fois avec plus de précision, et une fois sans plus grande précision.

De nombreux FPU peuvent être commutés sur un mode où ils ne renvoient pas de nombres dénormalisés mais les remplacent par 0. Dans ce mode, si a et b sont des nombres normalisés minuscules où la différence est inférieure au plus petit nombre normalisé mais supérieur à 0, un ! = b ne garantit pas non plus un == b.

“Ne jamais comparer les nombres à virgule flottante” est la programmation de cargo cargo. Parmi les personnes qui ont le mantra “vous avez besoin d’un epsilon”, la plupart n’ont aucune idée de la manière de choisir correctement cet epsilon.

Vous ne devriez jamais comparer des flotteurs ou des doubles pour l’égalité; parce que vous ne pouvez pas vraiment garantir que le numéro que vous atsortingbuez au float ou au double est exact.

Pour comparer les flottants de manière équitable, vous devez vérifier si la valeur est “suffisamment proche” de la même valeur:

 if ((first >= second - error) || (first <= second + error) 

Je peux penser à un cas où vous pourriez être en mesure de provoquer cela. Voici un échantillon analogue en base 10 – cela se produirait bien sûr en base 2.

Les nombres à virgule flottante sont stockés plus ou moins en notation scientifique – c’est-à-dire qu’au lieu de voir 35.2, le nombre stocké serait plus proche de 3.52e2.

Imaginez par souci de commodité que nous ayons une unité à virgule flottante qui fonctionne en base 10 et qui possède 3 chiffres de précision. Que se passe-t-il lorsque vous soustrayez 9,99 de 10,0?

1.00e2-9.99e1

Shift pour donner à chaque valeur le même exposant

1.00e2-0.999e2

Arrondir à 3 chiffres

1.00e2-1.00e2

Uh oh!

Que cela puisse se produire dépend en fin de compte de la conception du FPU. Etant donné que la gamme des exposants pour un double est très grande, le matériel doit arrondir en interne à un moment donné, mais dans le cas ci-dessus, un seul chiffre supplémentaire en interne empêchera tout problème.

Sur la base de la réponse de @malarres et du commentaire de @Taemyr, voici ma petite consortingbution:

 public double calculation(double a, double b) { double c = 2 / (a - b); // Should not have a big cost. if (isnan(c) || isinf(c)) { return 0; // A 'whatever' value. } else { return c; } } 

Mon propos est de dire: le moyen le plus simple de savoir si le résultat de la division est nan ou inf est de réaliser la division.

La division par zéro n’est pas définie, puisque la limite des nombres positifs tend vers l’infini, les nombres limités des nombres négatifs ont tendance à l’infini négatif.

Vous ne savez pas s’il s’agit de C ++ ou de Java car il n’y a pas de balise de langue.

 double calculation(double a, double b) { if (a == b) { return nan(""); // C++ return Double.NaN; // Java } else { return 2 / (a - b); } } 

Le problème principal est que la représentation par ordinateur d’un double (alias float, ou nombre réel en langage mathématique) est incorrecte lorsque vous avez trop de décimales, par exemple lorsque vous traitez avec du double qui ne peut pas être écrit en tant que valeur numérique ( pi ou le résultat de 1/3).

Donc, un == b ne peut pas être fait avec une valeur double de a et b, comment traiter un == b lorsque a = 0,333 et b = 1/3? Selon votre système d’exploitation vs FPU vs nombre vs langue par rapport à 3 après 0, vous aurez vrai ou faux.

Quoi qu’il en soit, si vous effectuez un “calcul de valeur double” sur un ordinateur, vous devez gérer la précision, donc au lieu de faire a==b , vous devez faire la absolute_value(ab) et epsilon cette fois dans votre algorithme. Vous ne pouvez pas avoir une valeur epsilon pour toute votre double comparaison.

En bref, lorsque vous tapez un == b, vous avez une expression mathématique qui ne peut pas être traduite sur un ordinateur (pour tout nombre à virgule flottante).

PS: hum, tout ce que je réponds ici est plus ou moins dans les autres réponses et commentaires.