Vraag Drijvende punt gelijkheid


Het is algemeen bekend dat men voorzichtig moet zijn bij het vergelijken van drijvende-kommawaarden. Meestal in plaats van gebruiken ==, we gebruiken enkele op epsilon of ULP gebaseerde gelijkheidstests.

Ik vraag me echter af, zijn er gevallen, bij het gebruik == is prima?

Bekijk dit eenvoudige fragment, welke cases zijn gegarandeerd succesvol?

void fn(float a, float b) {
    float l1 = a/b;
    float l2 = a/b;

    if (l1==l1) { }        // case a)
    if (l1==l2) { }        // case b)
    if (l1==a/b) { }       // case c)
    if (l1==5.0f/3.0f) { } // case d)
}

int main() {
    fn(5.0f, 3.0f);
}

Opmerking: ik heb het nagevraagd deze en deze, maar ze zijn niet van toepassing op (al mijn) zaken.

Opmerking 2: Het lijkt erop dat ik wat positieve informatie moet toevoegen, zodat antwoorden in de praktijk nuttig kunnen zijn: ik zou graag willen weten:

  • wat de C ++ standaard zegt
  • wat er gebeurt als een C ++ -implementatie volgt op IEEE-754

Dit is de enige relevante verklaring die ik heb gevonden in de huidige conceptnorm:

De waardeweergave van drijvende-kommatypes is door de implementatie gedefinieerd. [Opmerking: dit document stelt geen eisen aan de nauwkeurigheid van drijvende-kommabewerkingen; zie ook [support.limits]. - eindnoot]

Dus, betekent dit dat zelfs "geval a)" implementatie is gedefinieerd? Ik bedoel, l1==l1 is zeker een drijvende-kommabewerking. Dus als een implementatie 'onnauwkeurig' is, zou dat kunnen l1==l1 niet waar zijn?


Ik denk dat deze vraag geen duplicaat is Is floating-point == ooit OK?. Die vraag behandelt geen van de gevallen die ik vraag. Hetzelfde onderwerp, andere vraag. Ik zou graag specifieke antwoorden hebben op case a) -d), waarvoor ik in de gedupliceerde vraag geen antwoorden kan vinden.


47
2017-07-02 10:26


oorsprong


antwoorden:


Ik vraag me echter af, zijn er gevallen, wanneer het gebruik van == prima is?

Zeker dat er zijn. Eén categorie voorbeelden zijn gebruikswijzen die geen berekening met zich meebrengen, b.v. setters die alleen moeten uitvoeren op wijzigingen:

void setRange(float min, float max)
{
    if(min == m_fMin && max == m_fMax)
        return;

    m_fMin = min;
    m_fMax = max;

    // Do something with min and/or max
    emit rangeChanged(min, max);
}

Zie ook Is floating-point == ooit OK? en Is floating-point == ooit OK?.


16
2017-07-02 11:12



Ingepaste gevallen kunnen "werken". Praktische gevallen kunnen nog steeds falen. Een bijkomend probleem is dat vaak optimalisatie kleine variaties in de berekening veroorzaakt, zodat symbolisch de resultaten gelijk moeten zijn, maar ze zijn numeriek anders. Het bovenstaande voorbeeld zou in een dergelijk geval theoretisch kunnen falen. Sommige compilers bieden een optie om consistentere resultaten te produceren, wat de prestaties ten goede komt. Ik zou adviseren "altijd" de gelijkheid van drijvende-kommagetallen te vermijden.

Gelijkheid van fysieke metingen, evenals digitaal opgeslagen vlotters, is vaak zinloos. Dus als je vergelijkt of floats gelijk zijn in je code, doe je waarschijnlijk iets verkeerd. U wilt meestal meer dan of minder dat of binnen een tolerantie. Vaak kan de code worden herschreven, zodat dit soort problemen wordt voorkomen.


7
2017-07-02 11:27



Alleen a) en b) zijn gegarandeerd succesvol in elke gezonde implementatie (zie de legalese hieronder voor details), omdat ze twee waarden vergelijken die op dezelfde manier zijn afgeleid en afgerond naar float precisie. Bijgevolg zijn beide vergeleken waarden gegarandeerd identiek aan het laatste bit.

Geval c) en d) kunnen mislukken omdat de berekening en daaropvolgende vergelijking met een hogere precisie dan kunnen worden uitgevoerd float. De verschillende afronding van double zou voldoende moeten zijn om de test te mislukken.

Merk op dat de gevallen a) en b) mogelijk nog steeds mislukken als er oneindigheden of NAN's aan te pas komen.


legalese

Met de N3242 C ++ 11 werkversie van de standaard vind ik het volgende:

In de tekst die de toewijzingsuitdrukking beschrijft, wordt expliciet vermeld dat typeconversie plaatsvindt, [expr.ass] 3:

Als de linker-operand niet van het klassetype is, wordt de expressie impliciet geconverteerd (Clausule 4) naar het cv-niet-gekwalificeerde type van de linker-operand.

Clausule 4 verwijst naar de standaardconversies [conv], die het volgende bevatten bij zwevende-kommaconversies, [conv.double] 1:

Een prvalue met drijvende-kommatype kan worden geconverteerd naar een prvalue van een ander drijvende-kommatype. Als het   de bronwaarde kan exact worden weergegeven in het bestemmingstype, het resultaat van de conversie is dat exact   vertegenwoordiging. Als de bronwaarde tussen twee aangrenzende bestemmingswaarden ligt, is het resultaat van de conversie   is een door de implementatie gedefinieerde keuze voor een van deze waarden. Anders is het gedrag niet gedefinieerd.

(Nadruk van mij.)

We hebben dus de garantie dat het resultaat van de conversie daadwerkelijk wordt gedefinieerd, tenzij we te maken hebben met waarden buiten het representeerbare bereik (zoals float a = 1e300, dat is UB).

Wanneer mensen denken dat 'interne drijvende-kommaweergave misschien preciezer is dan zichtbaar in code', denken ze na over de volgende zin in de standaard, [expr] 11:

De waarden van de zwevende operanden en de resultaten van zwevende expressies kunnen in groter worden weergegeven   precisie en bereik dan vereist door het type; de typen worden daardoor niet veranderd.

Merk op dat dit van toepassing is op operanden en resultaten, niet naar variabelen. Dit wordt benadrukt door de bijgevoegde voetnoot 60:

De cast- en toewijzingsoperators moeten nog steeds hun specifieke conversies uitvoeren zoals beschreven in 5.4, 5.2.9 en 5.17.

(Ik veronderstel dat dit de voetnoot is die Maciej Piechotka in de opmerkingen bedoelde - de nummering lijkt te zijn veranderd in de versie van de standaard die hij heeft gebruikt.)

Dus, als ik het zeg float a = some_double_expression;, Ik heb de garantie dat het resultaat van de expressie eigenlijk afgerond is om door een te worden gerepresenteerd float (UB alleen oproepen als de waarde out-of-bounds is), en a zal daarna naar die afgeronde waarde verwijzen.

Een implementatie zou inderdaad kunnen specificeren dat het resultaat van de afronding willekeurig is, en dus de gevallen a) en b) verbreken. Gezonde implementaties zullen dat echter niet doen.


5
2017-07-02 11:23



Uitgaande van de IEEE 754-semantiek, zijn er absoluut enkele gevallen waarin u dit kunt doen. Conventionele drijvende-komma-getalberekeningen zijn exact wanneer ze kunnen zijn, wat bijvoorbeeld (maar niet beperkt tot) alle basishandelingen omvat, waarbij de operanden en de resultaten gehele getallen zijn.

Dus als je zeker weet dat je niets doet dat zou resulteren in iets dat niet representeerbaar is, dan ben je in orde. Bijvoorbeeld

float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed

De situatie wordt pas echt slecht als je berekeningen hebt met resultaten die niet exact representeerbaar zijn (of die bewerkingen omvatten die niet exact zijn) en je de volgorde van bewerkingen verandert.

Merk op dat de C ++ -standaard zelf geen IEEE 754-semantiek garandeert, maar dat is wat u kunt verwachten het grootste deel van de tijd te verwerken.


2
2017-07-02 15:06



Geval (a) mislukt als a == b == 0.0. In dit geval levert de operatie NaN op, en per definitie (IEEE, niet C) NaN ≠ NaN.

Gevallen (b) en (c) kunnen mislukken in parallelle berekeningen wanneer drijvende-komma ronde modi (of andere berekeningsmodi) worden gewijzigd in het midden van de uitvoering van deze thread. Gezien deze in de praktijk helaas.

Geval (d) kan anders zijn omdat de compiler (op een bepaalde machine) ervoor kan kiezen om de berekening constant te vouwen 5.0f/3.0f en vervang het door het constante resultaat (van niet-gespecificeerde precisie), terwijl a/b moet tijdens runtime op de doelcomputer worden berekend (wat radicaal anders kan zijn). In feite kunnen tussentijdse berekeningen met willekeurige nauwkeurigheid worden uitgevoerd. Ik heb verschillen gezien in oude Intel-architecturen toen tussentijdse berekening werd uitgevoerd in 80-bits floating-point, een formaat dat de taal zelfs niet direct ondersteunde.


2
2017-07-04 01:46



Naar mijn bescheiden mening moet u niet vertrouwen op de == operator omdat deze veel hoekgevallen heeft. Het grootste probleem is afronding en uitgebreide precisie. In het geval van x86 kunnen drijvende-kommabewerkingen met grotere precisie worden uitgevoerd dan u in variabelen kunt opslaan (als u coprocessors, IIRC gebruikt SSE bewerkingen gebruiken dezelfde precisie als opslag).

Dit is meestal goed, maar dit veroorzaakt problemen zoals: 1./2 != 1./2 omdat een waarde de vormvariabele is en de tweede van een drijvende komma-register is. In de eenvoudigste gevallen werkt het, maar als u andere drijvende-kommabewerkingen toevoegt, kan de compiler besluiten om een ​​aantal variabelen naar de stapel te splitsen, hun waarden te wijzigen en zo het resultaat van de vergelijking te wijzigen.

Om 100% zekerheid te hebben, moet u de assemblage bekijken en zien welke bewerkingen eerder werden uitgevoerd voor beide waarden. Zelfs de volgorde kan het resultaat veranderen in niet-triviale gevallen.

Over het algemeen wat het nut is om te gebruiken ==? U moet algoritmen gebruiken die stabiel zijn. Dit betekent dat ze werken, zelfs als waarden niet gelijk zijn, maar ze geven nog steeds dezelfde resultaten. De enige plek waar ik weet waar == nuttig kan zijn is serialiseren / deserialiseren, waarbij u weet welk resultaat u precies wilt en u de serialisatie kunt wijzigen om uw doel te archiveren.


1
2017-07-02 11:44