Question

Quelle est la meilleure méthode pour comparer les flottants et les doubles IEEE pour l'égalité ?J'ai entendu parler de plusieurs méthodes, mais je voulais voir ce qu'en pensait la communauté.

Était-ce utile?

La solution

La meilleure approche, je pense, est de comparer ULP.

bool is_nan(float f)
{
    return (*reinterpret_cast<unsigned __int32*>(&f) & 0x7f800000) == 0x7f800000 && (*reinterpret_cast<unsigned __int32*>(&f) & 0x007fffff) != 0;
}

bool is_finite(float f)
{
    return (*reinterpret_cast<unsigned __int32*>(&f) & 0x7f800000) != 0x7f800000;
}

// if this symbol is defined, NaNs are never equal to anything (as is normal in IEEE floating point)
// if this symbol is not defined, NaNs are hugely different from regular numbers, but might be equal to each other
#define UNEQUAL_NANS 1
// if this symbol is defined, infinites are never equal to finite numbers (as they're unimaginably greater)
// if this symbol is not defined, infinities are 1 ULP away from +/- FLT_MAX
#define INFINITE_INFINITIES 1

// test whether two IEEE floats are within a specified number of representable values of each other
// This depends on the fact that IEEE floats are properly ordered when treated as signed magnitude integers
bool equal_float(float lhs, float rhs, unsigned __int32 max_ulp_difference)
{
#ifdef UNEQUAL_NANS
    if(is_nan(lhs) || is_nan(rhs))
    {
        return false;
    }
#endif
#ifdef INFINITE_INFINITIES
    if((is_finite(lhs) && !is_finite(rhs)) || (!is_finite(lhs) && is_finite(rhs)))
    {
        return false;
    }
#endif
    signed __int32 left(*reinterpret_cast<signed __int32*>(&lhs));
    // transform signed magnitude ints into 2s complement signed ints
    if(left < 0)
    {
        left = 0x80000000 - left;
    }
    signed __int32 right(*reinterpret_cast<signed __int32*>(&rhs));
    // transform signed magnitude ints into 2s complement signed ints
    if(right < 0)
    {
        right = 0x80000000 - right;
    }
    if(static_cast<unsigned __int32>(std::abs(left - right)) <= max_ulp_difference)
    {
        return true;
    }
    return false;
}

Une technique similaire peut être utilisée pour les doubles.L'astuce consiste à convertir les flottants afin qu'ils soient ordonnés (comme s'il s'agissait d'entiers), puis à voir à quel point ils sont différents.

Je n'ai aucune idée de pourquoi cette foutue chose gâche mes traits de soulignement.Modifier:Oh, c'est peut-être juste un artefact de l'aperçu.C'est bon alors.

Autres conseils

La version actuelle que j'utilise est la suivante

bool is_equals(float A, float B,
               float maxRelativeError, float maxAbsoluteError)
{

  if (fabs(A - B) < maxAbsoluteError)
    return true;

  float relativeError;
  if (fabs(B) > fabs(A))
    relativeError = fabs((A - B) / B);
  else
    relativeError = fabs((A - B) / A);

  if (relativeError <= maxRelativeError)
    return true;

  return false;
}

Cela semble résoudre la plupart des problèmes en combinant la tolérance aux erreurs relative et absolue.L’approche ULP est-elle meilleure ?Si oui, pourquoi?

@DrPizza :Je ne suis pas un gourou de la performance, mais je m'attendrais à ce que les opérations en virgule fixe soient plus rapides que les opérations en virgule flottante (dans la plupart des cas).

Cela dépend plutôt de ce que vous en faites.Un type à virgule fixe avec la même plage qu'un flottant IEEE serait plusieurs fois plus lent (et plusieurs fois plus grand).

Choses adaptées aux flotteurs :

Graphiques 3D, physique/ingénierie, simulation, simulation climatique....

Dans les logiciels numériques, vous souhaitez souvent tester si deux nombres à virgule flottante sont exactement égal.LAPACK regorge d’exemples de tels cas.Bien sûr, le cas le plus courant est celui où vous souhaitez tester si un nombre à virgule flottante est égal à "Zéro", "Un", "Deux", "Demi".Si quelqu'un est intéressé, je peux choisir quelques algorithmes et entrer plus en détail.

Également dans BLAS, vous souhaitez souvent vérifier si un nombre à virgule flottante est exactement zéro ou un.Par exemple, la routine dgemv peut calculer des opérations de la forme

  • y = bêta*y + alpha*A*x
  • y = bêta*y + alpha*A^T*x
  • y = bêta*y + alpha*A^H*x

Donc, si bêta est égal à Un, vous avez une "affectation plus" et pour bêta est égal à Zéro, une "affectation simple".Vous pouvez donc certainement réduire le coût de calcul si vous accordez un traitement spécial à ces cas (courants).

Bien sûr, vous pouvez concevoir les routines BLAS de manière à éviter les comparaisons exactes (par ex.en utilisant quelques drapeaux).Cependant, le LAPACK regorge d’exemples où cela n’est pas possible.

P.S. :

  • Il existe certainement de nombreux cas où vous ne souhaitez pas vérifier "est exactement égal".Pour beaucoup de gens, cela pourrait même être le seul cas auquel ils soient confrontés.Tout ce que je veux souligner, c'est qu'il existe également d'autres cas.

  • Bien que LAPACK soit écrit en Fortran, la logique est la même si vous utilisez d'autres langages de programmation pour les logiciels numériques.

Oh mon Dieu, s'il vous plaît, n'interprètez pas les bits flottants comme des entiers, sauf si vous utilisez un P6 ou une version antérieure.

Même si cela l'oblige à copier des registres vectoriels vers des registres entiers via la mémoire, et même si cela bloque le pipeline, c'est la meilleure façon de le faire que j'ai rencontrée, dans la mesure où elle fournit les comparaisons les plus robustes, même en face. d'erreurs en virgule flottante.

c'est à dire.c'est un prix qui vaut la peine d'être payé.

Cela semble résoudre la plupart des problèmes en combinant la tolérance aux erreurs relative et absolue.L’approche ULP est-elle meilleure ?Si oui, pourquoi?

Les ULP sont une mesure directe de la « distance » entre deux nombres à virgule flottante.Cela signifie qu'ils ne vous obligent pas à évoquer les valeurs d'erreur relatives et absolues, ni à vous assurer que ces valeurs sont "à peu près correctes".Avec les ULP, vous pouvez exprimer directement à quel point vous souhaitez que les nombres soient proches, et le même seuil fonctionne aussi bien pour les petites valeurs que pour les grandes.

Si vous avez des erreurs en virgule flottante, vous avez encore plus de problèmes que cela.Même si je suppose que cela dépend de votre point de vue personnel.

Même si nous effectuons l'analyse numérique pour minimiser l'accumulation d'erreurs, nous ne pouvons pas l'éliminer et nous pouvons nous retrouver avec des résultats qui devraient être identiques (si nous calculions avec des réels) mais différents (car nous ne pouvons pas calculer avec des réels).

Si vous recherchez que deux flotteurs soient égaux, alors ils devraient être identiques à mon avis.Si vous êtes confronté à un problème d'arrondi en virgule flottante, une représentation en virgule fixe conviendrait peut-être mieux à votre problème.

Si vous recherchez que deux flotteurs soient égaux, alors ils devraient être identiques à mon avis.Si vous êtes confronté à un problème d'arrondi en virgule flottante, une représentation en virgule fixe conviendrait peut-être mieux à votre problème.

Peut-être ne pouvons-nous pas nous permettre la perte de portée ou de performances qu’une telle approche entraînerait.

@DrPizza :Je ne suis pas un gourou de la performance, mais je m'attendrais à ce que les opérations en virgule fixe soient plus rapides que les opérations en virgule flottante (dans la plupart des cas).

@Craig H :Bien sûr.Je suis tout à fait d'accord avec l'impression de ça.Si a ou b stockent de l’argent, ils doivent être représentés en virgule fixe.J'ai du mal à penser à un exemple réel où une telle logique devrait être alliée aux flotteurs.Choses adaptées aux flotteurs :

  • poids
  • rangs
  • distances
  • valeurs du monde réel (comme celles d'un ADC)

Pour toutes ces choses, soit vous numérotez et présentez simplement les résultats à l'utilisateur pour une interprétation humaine, soit vous faites une déclaration comparative (même si une telle déclaration est "cette chose est à moins de 0,001 de cette autre chose").Une déclaration comparative comme la mienne n’est utile que dans le contexte de l’algorithme :la partie "à 0,001 près" dépend de ce que physique question que vous posez.C'est mon 0,02.Ou devrais-je dire 2/100ème ?

Cela dépend plutôt de ce que vous faites avec eux.Un type de point fixe avec la même plage qu'un flotteur IEEE serait plusieurs fois plus lent (et plusieurs fois plus grand).

D'accord, mais si je veux une résolution binaire infinitésimale, je reviens à mon point de départ :== et != n'ont aucune signification dans le contexte d'un tel problème.

Un int me permet d'exprimer ~ 10 ^ 9 valeurs (quelle que soit la plage), ce qui semble suffisant pour toute situation où je voudrais que deux d'entre elles soient égales.Et si cela ne suffit pas, utilisez un système d'exploitation 64 bits et vous obtenez environ 10 ^ 19 valeurs distinctes.

Je peux exprimer des valeurs comprises entre 0 et 10 ^ 200 (par exemple) dans un int, c'est juste la résolution en bits qui en souffre (la résolution serait supérieure à 1, mais, encore une fois, aucune application n'a également ce type de plage comme ce genre de résolution).

Pour résumer, je pense que dans tous les cas, soit on représente un continuum de valeurs, auquel cas != et == ne sont pas pertinents, soit on représente un ensemble fixe de valeurs, qui peuvent être mappées à un int (ou à un autre fixe -type de précision).

Un int me permet d'exprimer ~ 10 ^ 9 des valeurs (quelle que soit la plage), ce qui semble suffisant pour toute situation où je me soucierais que deux d'entre elles sont égales.Et si cela ne suffit pas, utilisez un système d'exploitation 64 bits et vous avez environ 10 ^ 19 valeurs distinctes.

En fait, j'ai atteint cette limite...J'essayais de jongler avec les temps en ps et le temps en cycles d'horloge dans une simulation où vous atteignez facilement 10 ^ 10 cycles.Peu importe ce que je faisais, j'ai très rapidement dépassé la plage chétive des entiers de 64 bits...10 ^ 19 n'est pas autant que vous le pensez, donnez-moi de l'informatique 128 bits maintenant !

Les flotteurs m'ont permis d'obtenir une solution aux problèmes mathématiques, car les valeurs débordaient de nombreux zéros dans le bas de l'échelle.Donc, en gros, vous aviez une virgule décimale flottant autour du nombre sans perte de précision (je pourrais aimer le nombre distinct plus limité de valeurs autorisées dans la mantisse d'un flottant par rapport à un entier de 64 bits, mais j'avais désespérément besoin de la plage ! ).

Et puis les choses sont reconverties en nombres entiers pour comparer, etc.

Ennuyeux, et à la fin, j'ai abandonné toute la tentative et je me suis simplement appuyé sur les flotteurs et < et > pour faire le travail.Pas parfait, mais fonctionne pour le cas d'utilisation envisagé.

Si vous recherchez que deux flotteurs soient égaux, alors ils devraient être identiques à mon avis.Si vous êtes confronté à un problème d'arrondi en virgule flottante, une représentation en virgule fixe conviendrait peut-être mieux à votre problème.

Peut-être devrais-je mieux expliquer le problème.En C++, le code suivant :

#include <iostream>

using namespace std;


int main()
{
  float a = 1.0;
  float b = 0.0;

  for(int i=0;i<10;++i)
  {
    b+=0.1;
  }

  if(a != b)
  {
    cout << "Something is wrong" << endl;
  }

  return 1;
}

imprime la phrase « Quelque chose ne va pas ».Êtes-vous en train de dire que cela devrait être le cas ?

Oh mon Dieu, s'il vous plaît, n'interprètez pas les bits flottants comme des entiers, sauf si vous utilisez un P6 ou une version antérieure.

c'est la meilleure façon de le faire que j'ai rencontrée, dans la mesure où elle fournit les comparaisons les plus robustes, même face aux erreurs en virgule flottante.

Si vous avez des erreurs en virgule flottante, vous avez encore plus de problèmes que cela.Même si je suppose que cela dépend de votre point de vue personnel.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top