Is floating-point == ooit in orde?

Vandaag kwam ik software van derden tegen die we gebruiken en in hun voorbeeldcode stond iets in de trant van:

// Defined in somewhere.h
static const double BAR = 3.14;
// Code elsewhere.cpp
void foo(double d)
{
    if (d == BAR)
        ...
}

Ik ben me bewust van het probleem met drijvende-komma’s en hun representatie, maar ik vroeg me af of er gevallen zijn waarin float == floatgoed zou zijn? Ik vraag niet wanneer het zoukan werken, maar wanneer het zinvol is en werkt.

En hoe zit het met een oproep als foo(BAR)? Zal dit altijd gelijk zijn aangezien ze allebei dezelfde static const BARgebruiken?


Antwoord 1, autoriteit 100%

Ja, u bent er zeker van dat gehele getallen, inclusief 0,0, vergelijkbaar zijn met ==

Natuurlijk moet je een beetje voorzichtig zijn met hoe je het hele getal hebt gekregen, de toewijzing is veilig, maar het resultaat van elke berekening is verdacht

ps er is een reeks reële getallen die wel een perfecte reproductie hebben als float (denk aan 1/2, 1/4 1/8 etc) maar je weet waarschijnlijk niet van tevoren dat je een van deze hebt .

Even ter verduidelijking. IEEE 754 garandeert dat float-representaties van gehele getallen (hele getallen) binnen het bereik exact zijn.

float a=1.0;
float b=1.0;
a==b  // true

Maar je moet voorzichtig zijn hoe je de hele getallen krijgt

float a=1.0/3.0;
a*3.0 == 1.0  // not true !!

Antwoord 2, autoriteit 89%

Er zijn twee manieren om deze vraag te beantwoorden:

  1. Zijn er gevallen waarin float == floathet juiste resultaat geeft?
  2. Zijn er gevallen waarin float == floatacceptabele codering is?

Het antwoord op (1) is: Ja, soms. Maar het wordt kwetsbaar, wat leidt tot het antwoord op (2): Nee. Doe dat niet. Je smeekt om bizarre bugs in de toekomst.

Wat betreft een aanroep van de vorm foo(BAR): in dat specifieke geval zal de vergelijking true retourneren, maar wanneer u fooschrijft, weet u het niet (en zou niet moeten afhangen van) hoe het wordt genoemd. Het is bijvoorbeeld prima om foo(BAR)te bellen, maar foo(BAR * 2.0 / 2.0)(of misschien zelfs foo(BAR * 1.0)afhankelijk van hoeveel de compiler de dingen optimaliseert) zal breken. U moet er niet op vertrouwen dat de beller geen rekenkunde uitvoert!

Lang verhaal kort, hoewel a == bin sommige gevallen zal werken, moet je er echt niet op vertrouwen. Zelfs als je de belsemantiek vandaag kunt garanderen, kun je ze volgende week misschien niet garanderen, dus bespaar jezelf wat pijn en gebruik ==niet.

Naar mijn mening is float == floatnooit* OK omdat het vrijwel onhoudbaar is.

*Voor kleine waarden van nooit.


Antwoord 3, autoriteit 41%

De andere antwoorden verklaren heel goed waarom het gebruik van ==voor drijvende-kommagetallen gevaarlijk is. Ik heb net een voorbeeld gevonden dat deze gevaren vrij goed illustreert, geloof ik.

Op het x86-platform kun je rare drijvende-kommaresultaten krijgen voor sommige berekeningen, die niette wijten zijn aan afrondingsproblemen die inherent zijn aan de berekeningen die je uitvoert. Dit eenvoudige C-programma zal soms “error” afdrukken:

#include <stdio.h>
void test(double x, double y)
{
  const double y2 = x + 1.0;
  if (y != y2)
    printf("error\n");
}
void main()
{
  const double x = .012;
  const double y = x + 1.0;
  test(x, y);
}

Het programma berekent in wezen gewoon

x = 0.012 + 1.0;
y = 0.012 + 1.0;

(alleen verspreid over twee functies en met tussenliggende variabelen), maar de vergelijking kan nog steeds false opleveren!

De reden is dat programma’s op het x86-platform meestal de x87FPU gebruiken voor zwevende punt berekeningen. De x87 berekent intern met een hogere precisie dan gewone double, dus doublewaarden moeten worden afgerond wanneer ze in het geheugen worden opgeslagen. Dat betekent dat een roundtrip x87 -> RAM-> x87 verliest precisie, en dus verschillen de berekeningsresultaten afhankelijk van het feit of tussenresultaten via RAM zijn doorgegeven of dat ze allemaal in FPU-registers zijn gebleven. Dit is natuurlijk een beslissing van de compiler, dus de bug manifesteert zich alleen voor bepaalde compilers en optimalisatie-instellingen :-(.

Zie voor details de GCC-bug: http://gcc.gnu. org/bugzilla/show_bug.cgi?id=323

Nogal eng…

Aanvullende opmerking:

Dit soort bugs zijn over het algemeen vrij lastig te debuggen, omdat de verschillende waarden hetzelfde worden zodra ze in het RAM-geheugen terechtkomen.

Dus als u bijvoorbeeld het bovenstaande programma uitbreidt om de bitpatronen van yen y2direct na het vergelijken uit te printen, krijgt u de exacte dezelfde waarde. Om de waarde af te drukken, moet deze in het RAM worden geladen om te worden doorgegeven aan een afdrukfunctie zoals printf, en dat zal het verschil doen verdwijnen…


Antwoord 4, autoriteit 22%

Perfect voor integrale waarden, zelfs in drijvende-komma-indelingen

Maar het korte antwoord is: “Nee, niet gebruiken ==.”

Ironisch genoeg werkt het drijvende-komma-formaat “perfect”, d.w.z. met exacte precisie, wanneer het werkt op integrale waarden binnen het bereik van het formaat. Dit betekent dat als je je aan dubbelewaarden houdt, je perfect goede gehele getallen krijgt met iets meer dan 50 bits, wat je ongeveer +- 4.500.000.000.000.000.000 of 4,5 biljard geeft.

In feite is dit hoe JavaScript intern werkt, en dat is waarom JavaScript dingen als +en -kan doen op echt grote getallen, maar alleen <<en >>op 32-bits versies.

Strikt genomen kun je sommen en producten van getallen exact vergelijken met nauwkeurige representaties. Dat zijn alle gehele getallen, plus breuken die zijn samengesteld uit 1 / 2ntermen. Dus een lusverhoging met n + 0,25, n + 0,50,of n + 0,75zou prima zijn, maar geen van de andere 96 decimale breuken met 2 cijfers.

Het antwoord is dus: Hoewel exacte gelijkheid in theorie zinvol kan zijn in beperkte gevallen, kan dit het beste worden vermeden.


Antwoord 5, autoriteit 19%

Het enige geval waarin ik ooit ==(of !=) voor floats gebruik, is in het volgende:

if (x != x)
{
    // Here x is guaranteed to be Not a Number
}

en ik moet toegeven dat ik me schuldig maak aan het gebruik van Not A Number als een magische drijvende-kommaconstante (met behulp van numeric_limits<double>::quiet_NaN()in C++).

Het heeft geen zin om getallen met drijvende komma te vergelijken voor strikte gelijkheid. Drijvende-kommagetallen zijn ontworpen met voorspelbare relatieve nauwkeurigheidslimieten. Ubent verantwoordelijk om te weten welke precisie u van hen en uw algoritmen kunt verwachten.


Antwoord 6, autoriteit 19%

Ik zal proberen een min of meer echt voorbeeld te geven van legitieme, zinvolle en nuttige tests voor float-gelijkheid.

#include <stdio.h>
#include <math.h>
/* let's try to numerically solve a simple equation F(x)=0 */
double F(double x) {
    return 2*cos(x) - pow(1.2, x);
}
/* I'll use a well-known, simple&slow but extremely smart method to do this */
double bisection(double range_start, double range_end) {
    double a = range_start;
    double d = range_end - range_start;
    int counter = 0;
    while(a != a+d) // <-- WHOA!!
    {
        d /= 2.0;
        if(F(a)*F(a+d) > 0) /* test for same sign */
            a = a+d;
        ++counter;
    }
    printf("%d iterations done\n", counter);
    return a;
}
int main() {
    /* we must be sure that the root can be found in [0.0, 2.0] */
    printf("F(0.0)=%.17f, F(2.0)=%.17f\n", F(0.0), F(2.0));
    double x = bisection(0.0, 2.0);
    printf("the root is near %.17f, F(%.17f)=%.17f\n", x, x, F(x));
}

Ik zou liever niet de bisectiemethodezelf uitleggen, maar de nadruk leggen op het stoppen voorwaarde. Het heeft precies de besproken vorm: (a == a+d)waarbij beide zijden drijvers zijn: ais onze huidige benadering van de wortel van de vergelijking, en dis onze huidige precisie. Gezien de voorwaarde van het algoritme — dat er moeteen root zijn tussen range_starten range_endgaranderen weop elke iteratie dat de root tussen aen a+dblijft, terwijl dbij elke stap wordt gehalveerd, waardoor de grenzen kleiner worden.

En dan, na een aantal iteraties, wordt dzo kleindat tijdens het optellen met ahet naar nul wordt afgerond! Dat wil zeggen, a+dblijkt dichterbij ate zijn dan bij elke andere float; en dus rondt de FPU het af naar de dichtstbijzijnde waarde: naar de azelf. Dit kan eenvoudig worden geïllustreerd door berekening op een hypothetische rekenmachine; laat het een 4-cijferige decimale mantisse hebben en een groot exponentbereik. Welk resultaat zou de machine dan moeten geven aan 2.131e+02 + 7.000e-3? Het exacte antwoord is 213.107, maar onze machine kan zo’n getal niet weergeven; het moet er omheen. En 213.107ligt veel dichter bij 213.1dan bij 213.2— dus het afgeronde resultaat wordt 2.131e+02— de weinig summand verdween, naar boven afgerond op nul. Precies hetzelfde gegarandeerdgebeurt bij een of andere iteratie van ons algoritme – en op dat moment kunnen we niet meer verder. We hebben de wortel met de grootst mogelijke precisie gevonden.

De verheffende conclusie is blijkbaar dat drijvers lastig zijn. Ze lijken zoveel op echtegetallen dat elke programmeur in de verleiding komt om ze als echte getallen te zien. Maar dat zijn ze niet. Ze hebben hun eigen gedrag, dat enigszins doet denken aan echte‘s, maar niet helemaal hetzelfde. Je moet er heel voorzichtig mee zijn, vooral als je vergelijkt voor gelijkheid.


Bijwerken

Als ik het antwoord na een tijdje opnieuw bekijk, heb ik ook een interessant feit opgemerkt: in het bovenstaande algoritme kaneigenlijk “een klein getal”niet worden gebruikt in de stopconditie. Voor elke keuze van het nummer zullen er invoer zijn die uw keuze te grootzullen vinden, waardoor de precisie verloren gaat, ener zullen invoer zijn die uw keuze te klein, waardoor overmatige iteraties ontstaan ​​of zelfs in een oneindige lus terechtkomen. Gedetailleerde discussie volgt.

Misschien weet je al dat calculus geen idee heeft van een ‘klein getal’: voor elk reëel getal kun je gemakkelijk oneindig veel zelfs kleinere vinden. Het probleem is dat een van die “nog kleinere” misschien is wat we eigenlijk zoeken; het kan een wortel van onze vergelijking zijn. Erger nog, voor verschillende vergelijkingen kunnen er verschillendewortels zijn (bijv. 2.51e-8en 1.38e-8), beidewaarvan wordt benaderd door hetzelfdegetal als onze stopconditie eruit zou zien als d < 1e-6. Welk “klein getal” u ook kiest, veel wortels die met de maximale precisie correct zouden zijn gevonden met a == a+dstopconditie, worden verwend door de “epsilon” die is te groot.

Het is echter waar dat in getallen met drijvende komma de exponent een beperkt bereik heeft, dus je kunt eigenlijk het kleinsteniet-nul-positieve FP-nummer vinden (bijv. 1e-45denorm voor IEEE 754 enkele precisie FP). Maar het is nutteloos! while (d < 1e-45) {...}voor altijd in een lus zal blijven, uitgaande van enkele precisie (positief niet nul) d.

Afgezien van die pathologische randgevallen, elkekeuze van het “kleine aantal” in de d < epsstopvoorwaarde zal te kleinzijn voor veel vergelijkingen. In die vergelijkingen waar de wortel de exponent hoog genoeg heeft, zal het resultaat van aftrekking van twee mantissen die alleen verschillen in het minst significante cijfer gemakkelijk onze “epsilon” overschrijden. Bijvoorbeeld, met 6-cijferige mantissen 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000, wat betekent dat het kleinst mogelijke verschil tussen getallen met exponent +8 en 5-cijferige mantisse is… 1000! Wat nooit in bijvoorbeeld 1e-4zal passen. Voor deze getallen met (relatief) hoge exponent hebben we simpelweg niet genoeg precisie om ooit een verschil van 1e-4te zien.

Mijn implementatie hierboven hield ook rekening met dit laatste probleem, en je kunt zien dat delke stap wordt gehalveerd, in plaats van opnieuw te worden berekend als een verschil van (mogelijk enorm in exponent) aen b. Dus als we de stopvoorwaarde wijzigen in d < eps, het algoritme zal niet vastzitten in een oneindige lus met enorme wortels (het zou heel goed kunnen met (b-a) < eps), maar zal nog steeds onnodige iteraties uitvoeren tijdens het verkleinen van dbeneden de precisie van a.

Dit soort redenering lijkt misschien overdreven theoretisch en nodeloos diep, maar het doel is om nogmaals de trucjes van drijvers te illustreren. Je moet heel voorzichtig zijn met hun eindige precisie bij het schrijven van rekenkundige operatoren om hen heen.


Antwoord 7, autoriteit 11%

Het is waarschijnlijk oké als je de waarde nooit gaat berekenen voordat je hem vergelijkt. Als je aan het testen bent of een getal met drijvende komma precies pi, of -1, of 1 is en je weet dat dit de beperkte waarden zijn die worden doorgegeven in…


Antwoord 8, autoriteit 5%

Ik heb het ook een paar keer gebruikt bij het herschrijven van enkele algoritmen naar versies met meerdere threads. Ik heb een test gebruikt die de resultaten voor single- en multithreaded versies vergeleek om er zeker van te zijn dat beide exacthetzelfde resultaat geven.


Antwoord 9, autoriteit 5%

Naar mijn mening is vergelijken voor gelijkheid (of enige gelijkwaardigheid) in de meeste situaties een vereiste: standaard C++-containers of algoritmen met een impliciete functie voor het vergelijken van gelijkheid, zoals std::unordered_set bijvoorbeeld, vereisen dat deze comparator een equivalentierelatie is (zie C++ genoemde vereisten: UnorderedAssociativeContainer).

Helaas, vergeleken met een epsilon zoals in abs(a - b) < epsilonlevert geen equivalentierelatie op omdat het transitiviteit verliest. Dit is hoogstwaarschijnlijk ongedefinieerd gedrag, met name twee ‘bijna gelijke’ getallen met drijvende komma kunnen verschillende hashes opleveren; dit kan de unordered_set in een ongeldige staat brengen.
Persoonlijk zou ik == meestal gebruiken voor drijvende punten, tenzijenige vorm van FPU-berekening zou zijn betrokken op operanden. Met containers en containeralgoritmen, waarbij alleen lezen/schrijven betrokken is, is == (of een equivalentierelatie) het veiligst.

abs(a - b) < epsilonis min of meer een convergentiecriterium vergelijkbaar met een limiet. Ik vind deze relatie nuttig als ik moet verifiëren dat er een wiskundige identiteit bestaat tussen twee berekeningen (bijvoorbeeld PV = nRT, of afstand = tijd * snelheid).

Kortom, gebruik ==als en alleen als er geen drijvende-kommaberekening plaatsvindt;
gebruik nooit abs(a-b) < eals een gelijkheidspredikaat;


Antwoord 10, autoriteit 3%

Ja. 1/xis geldig tenzij x==0. Je hebt hier geen onnauwkeurige test nodig. 1/0.00000001is prima in orde. Ik kan geen ander geval bedenken – je kunt tan(x)niet eens controleren op x==PI/2


11

Ik heb een tekenprogramma dat fundamenteel een drijvend punt voor zijn coördinatensysteem gebruikt, aangezien de gebruiker op een granulariteit / zoom mag werken. Het ding dat ze tekenen, bevat lijnen die kunnen worden gebogen op punten die door hen zijn gemaakt. Wanneer ze een punt bovenop een ander slepen, worden ze samengevoegd.

Om “juiste” zwevende puntvergelijking te doen, zou ik een aantal reeks moeten bedenken om de punten hetzelfde te overwegen. Omdat de gebruiker in het oneindige kan inzoomen en binnen dat bereik kan werken en aangezien ik niemand kon laten plegen om een ​​soort bereik te plegen, gebruiken we gewoon ‘==’ om te zien of de punten hetzelfde zijn. Af en toe zal er een probleem zijn waarin punten die precies hetzelfde moeten zijn, uit .000000000001 of iets (vooral rond 0,0) maar meestal werkt het prima. Het hoort moeilijk te zijn om punten samen te voegen zonder dat de snap hoe dan ook is ingeschakeld … of dat is tenminste hoe de originele versie werkte.

Het gooit af en toe de testgroep, maar dat is hun probleem: p

Hoe dan ook, er is een voorbeeld van een mogelijk redelijke tijd om ‘==’ te gebruiken. Het ding om op te merken is dat de beslissing minder over technische nauwkeurigheid is dan over de wensen van de klant (of gebrek daaraan) en gemak. Het is niet iets dat toch allemaal accuraat is. Dus wat als twee punten niet samenvoegen als je ze verwacht? Het is niet het einde van de wereld en zal niet van invloed zijn op ‘berekeningen’.

Other episodes