web-dev-qa-db-fra.com

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

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 pas le cas, bien sûr. Mais que faire si a et b sont très proches, peut (a-b) aboutit à être 0 en raison de la précision du calcul?

Notez que cette question est pour Java, mais je pense qu'elle s'appliquera à la plupart des langages de programmation.

131
Thirler

En Java, a - b N'est jamais égal à 0 Si a != b. En effet, Java rend obligatoires les opérations à virgule flottante IEEE 754 qui prennent en charge les nombres dénormalisés. D'après la spécification :

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

Si un FPU fonctionne avec nombres dénormalisés , la soustraction de 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, la prise en charge IEEE 754 est facultative.

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

131
nwellnhof

Comme solution de contournement, 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 n'importe quelle langue.

51
malarres

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

Maintenant, la seule façon a == b retournerait vrai 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 comparer vrai avec +0,0, et leurs modèles de bits sont différents.

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

25
Eran

Il n'y a aucun cas où une division par zéro peut se produire ici.

Solveur SMTZ prend en charge l'arithmétique précise à virgule flottante IEEE. Demandons à Z3 de trouver les 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 a b)) (fp.eq (fp.sub rm a b) +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 (dont cinq). Le résultat inclut également la possibilité que n'importe laquelle des variables en jeu soit NaN ou infini.

a == b Est implémenté en tant que qualité fp.eq Afin que +0f Et -0f Se comparent égaux. La comparaison avec zéro est également implémentée en utilisant fp.eq. Puisque la question vise à éviter une division par zéro, c'est la comparaison appropriée.

Si le test d'égalité avait été implémenté en utilisant l'égalité au niveau du bit, +0f Et -0f Auraient été un moyen de mettre a - b À zéro. Une version précédente incorrecte de cette réponse contient des détails de mode sur ce cas pour les curieux.

Z3 Online ne prend pas encore en charge la théorie FPA. Ce résultat a été obtenu en utilisant la dernière branche instable. Il peut être reproduit à l'aide des 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 smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

L'utilisation de Z3 pour répondre aux questions flottantes IEEE est agréable car il est difficile d'ignorer les 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 flottantes et entières mixtes telles que "cet algorithme particulier de int log2(float) est-il correct?".

17
usr

La fonction fournie peut en effet renvoyer 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(String[] 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 retourné même si le dénominateur est non nul.

12
D Krueger

Dans une implémentation à virgule flottante conforme à IEEE-754, chaque type à virgule flottante peut contenir des nombres dans deux formats. 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 donc la différence entre eux n'est 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 avoir des opérations sur de très petits nombres beaucoup plus lentes que les opérations sur d'autres valeurs, ou demander au processeur de considérer simplement les nombres trop petits pour un format normalisé comme 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 ralentirait le code. D'un autre côté, il est possible que certaines implémentations offrent des options permettant au code de s'exécuter plus rapidement en échange d'une gestion légèrement bâclée des valeurs qui, dans la plupart des cas, serait beaucoup trop petite pour avoir de l'importance (dans les cas où les valeurs sont trop petites pour avoir de l'importance, il peut être ennuyeux d'avoir des calculs avec eux qui prennent dix fois plus de temps que les calculs qui comptent , donc dans de nombreuses situations pratiques, le vidage à zéro est plus utile que l'arithmétique lente mais précise).

6
supercat

Dans les temps anciens avant IEEE 754, il était tout à fait possible que a! = B n'implique pas a-b! = 0 et vice versa. Ce fut l'une des raisons de créer IEEE 754 en premier lieu.

Avec IEEE 754 c'est presque garanti. Les compilateurs C ou C++ sont autorisés à effectuer une opération avec une précision plus élevée que nécessaire. 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 une plus grande précision et une fois sans une plus grande précision.

De nombreuses FPU peuvent être commutées dans un mode où elles ne renvoient pas de nombres dénormalisés mais les remplacent par 0. Dans ce mode, si a et b sont de petits nombres normalisés où la différence est plus petite que le plus petit nombre normalisé mais supérieure à 0, a ! = b ne garantit pas non plus a == b.

"Ne jamais comparer les nombres à virgule flottante" est une programmation culte du fret. Parmi les personnes qui ont le mantra "vous avez besoin d'un epsilon", la plupart ne savent pas comment choisir correctement cet epsilon.

5
gnasher729

Je peux penser à un cas où vous pourrait être en mesure de provoquer cela. Voici un exemple analogue en base 10 - vraiment, cela se produirait en base 2, bien sûr.

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 comme 3,52e2.

Imaginez pour des raisons de commodité que nous avons une unité à virgule flottante qui fonctionne en base 10 et a 3 chiffres de précision. Que se passe-t-il lorsque vous soustrayez 9,99 de 10,0?

1,00e2-9,99e1

Maj pour donner à chaque valeur le même exposant

1,00e2-0,999e2

Arrondi à 3 chiffres

1,00e2-1,00e2

Euh oh!

Si cela peut se produire en fin de compte, cela dépend de la conception du FPU. Étant donné que la gamme d'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 évitera tout problème.

2
Keldor314

Le problème principal est que la représentation informatique d'un double (aka float, ou nombre réel en langage mathématique) est erronée lorsque vous avez "trop" de décimales, par exemple lorsque vous traitez avec un double qui ne peut pas être écrit comme une valeur numérique ( pi ou le résultat de 1/3).

Donc, a == b ne peut pas être fait avec une double valeur de a et b, comment gérer a == b lorsque a = 0,333 et b = 1/3? En fonction de votre système d'exploitation vs FPU vs nombre vs langue par rapport au nombre de 3 après 0, vous aurez vrai ou faux.

Quoi qu'il en soit, si vous effectuez un "calcul de double valeur" sur un ordinateur, vous devez faire face à la précision, donc au lieu de faire a==b, Vous devez faire absolute_value(a-b)<epsilon, et epsilon est relatif à ce que vous modélisent à ce moment dans votre algorithme. Vous ne pouvez pas avoir une valeur epsilon pour l'ensemble de votre double comparaison.

En bref, lorsque vous tapez a == 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 encore plus ou moins dans d'autres réponses et commentaires.

1
Jean Davy

Vous ne devriez jamais comparer des flottants ou des doubles pour l'égalité; car, vous ne pouvez pas vraiment garantir que le numéro que vous attribuez au flottant ou au double est exact.

Pour comparer les flottants pour une égalité saine, vous devez vérifier si la valeur est "assez proche" de la même valeur:

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

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

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 point est de dire: le moyen le plus simple de savoir si le résultat de la division est nan ou inf est réellement d'effectuer la division.

1
Orace

La division par zéro n'est pas définie, car la limite des nombres positifs tend vers l'infini, la limite des nombres négatifs tend vers l'infini négatif.

Je ne sais pas s'il s'agit de C++ ou 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);
     }
}
1
Khaled.K