Wenn ich einen Float in eine andere Variable kopiere, sind sie dann gleich?

167

Ich weiß, dass die ==Überprüfung der Gleichheit von Gleitkommavariablen kein guter Weg ist. Aber ich möchte das nur mit den folgenden Aussagen wissen:

float x = ...

float y = x;

assert(y == x)

Wird die Behauptung wahr sein, da sie von ykopiert xwurde?

Wei Li
quelle
78
Lassen Sie mich jemandem eine Prämie von 50 gewähren, der durch eine Demonstration mit echtem Code tatsächlich Ungleichheit beweist. Ich möchte das 80 vs 64 Bit Ding in Aktion sehen. Plus 50 für eine Erklärung des generierten Assembler-Codes, der zeigt, dass sich eine Variable in einem Register befindet und die andere nicht (oder was auch immer der Grund für die Ungleichung sein mag, ich möchte, dass es auf einer niedrigen Ebene erklärt wird).
Thomas Weller
1
@ThomasWeller der GCC-Fehler dazu: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; Ich habe jedoch gerade versucht, es auf einem x86-64-System zu reproduzieren, und das tut es auch mit -ffast-math nicht. Ich vermute, Sie benötigen ein altes GCC auf einem 32-Bit-System.
pjc50
5
@ pjc50: Eigentlich brauchst du ein 80-Bit-System, um den Fehler 323 zu reproduzieren; Es ist die 80x87-FPU, die das Problem verursacht hat. x86-64 verwendet die SSE-FPU. Die zusätzlichen Bits verursachen das Problem, da sie gerundet werden, wenn ein Wert auf einen 32-Bit-Float verschüttet wird.
MSalters
4
Wenn die Theorie von MSalters korrekt ist (und ich vermute, dass dies der Fall ist), können Sie die Reproduktion entweder durch Kompilieren für 32-Bit ( -m32) oder durch Anweisung von GCC zur Verwendung der x87-FPU ( -mfpmath=387) durchführen.
Cody Gray
4
Ändern Sie "48 Bit" in "80 Bit", und entfernen Sie dort das "mythische" Adjektiv @Hot. Genau das wurde unmittelbar vor Ihrem Kommentar besprochen. Die x87 (FPU für x86-Architektur) verwendet 80-Bit-Register, ein Format mit "erweiterter Genauigkeit".
Cody Gray

Antworten:

125

Neben dem assert(NaN==NaN);von kmdreko aufgezeigten Fall können mit x87-math Situationen auftreten, in denen 80-Bit-Floats vorübergehend im Speicher gespeichert und später mit Werten verglichen werden, die noch in einem Register gespeichert sind.

Mögliches minimales Beispiel, das beim Kompilieren mit gcc9.2 fehlschlägt mit -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Godbolt Demo: https://godbolt.org/z/X-Xt4R

Das volatile kann wahrscheinlich weggelassen werden, wenn Sie es schaffen, einen ausreichenden Registerdruck zu erzeugen, um ihn zu yspeichern und aus dem Speicher neu zu laden (aber den Compiler genug zu verwirren, um den Vergleich nicht insgesamt wegzulassen).

Siehe GCC FAQ Referenz:

chtz
quelle
2
Es scheint seltsam, dass die zusätzlichen Bits beim Vergleich von a floatmit Standardgenauigkeit mit zusätzlicher Genauigkeit berücksichtigt werden .
Nat
13
@ Nat Es ist seltsam; Das ist ein Fehler .
Leichtigkeitsrennen im Orbit
13
@ ThomasWeller Nein, das ist eine vernünftige Auszeichnung. Obwohl ich möchte, dass die Antwort darauf hinweist, dass dies ein nicht konformes Verhalten ist
Lightness Races in Orbit
4
Ich kann diese Antwort erweitern und darauf hinweisen, was genau im Assembler-Code passiert und dass dies tatsächlich gegen den Standard verstößt - obwohl ich mich nicht als Sprachanwalt bezeichnen würde, kann ich nicht garantieren, dass es keine Dunkelheit gibt Klausel, die dieses Verhalten explizit zulässt. Ich gehe davon aus, dass das OP mehr an praktischen Komplikationen bei tatsächlichen Compilern interessiert war, nicht an vollständig fehlerfreien, vollständig kompatiblen Compilern (die de facto nicht existieren, denke ich).
14.
4
Erwähnenswert -ffloat-storescheint dies der Weg zu sein, um dies zu verhindern.
OrangeDog
116

Es wird nicht wahr sein , wenn xist NaN, da Vergleiche NaNsind immer falsch (ja, auch NaN == NaN). Für alle anderen Fälle (Normalwerte, Subnormalwerte, Unendlichkeiten, Nullen) ist diese Behauptung wahr.

Der Rat zur Vermeidung von Gleitkommazahlen ==gilt für Berechnungen, da Gleitkommazahlen nicht viele Ergebnisse genau ausdrücken können, wenn sie in arithmetischen Ausdrücken verwendet werden. Die Zuweisung ist keine Berechnung und es gibt keinen Grund, warum die Zuweisung einen anderen Wert als das Original ergeben würde.


Eine Bewertung mit erweiterter Genauigkeit sollte kein Problem darstellen, wenn der Standard eingehalten wird. Von <cfloat>von C geerbt [5.2.4.2.2.8] ( Schwerpunkt Mine ):

Mit Ausnahme von Zuweisung und Umwandlung (die alle zusätzlichen Bereiche und Genauigkeiten entfernen) werden die Werte von Operationen mit schwebenden Operanden und Werten, die den üblichen arithmetischen Konvertierungen unterliegen, und von schwebenden Konstanten in einem Format ausgewertet, dessen Bereich und Genauigkeit größer sein können als von der gefordert Art.

Da jedoch die Kommentare haben darauf hingewiesen, einige Fälle mit bestimmten Compilern, Build-Optionen und Ziele könnten machen dies paradoxerweise falsch.

kmdreko
quelle
10
Was passiert , wenn xin einem Register in der ersten Zeile berechnet wird , für eine höhere Präzision als das Minimum zu halten float. Das y = xkann im Speicher sein und nur floatPräzision behalten . Dann würde der Test auf Gleichheit mit dem Speicher gegen das Register mit unterschiedlichen Präzisionen und somit ohne Garantie durchgeführt.
David Schwartz
5
x+pow(b,2)==x+pow(a,3)kann davon abweichen, auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoweil einer mit größerer Genauigkeit als der andere vergleichen kann (wenn ein / zwei 64-Bit-Werte in RAM sind, während Zwischenwerte 80-Bit-Werte in fpu sind). Ein Auftrag kann also manchmal etwas bewirken.
Yakk - Adam Nevraumont
22
@evg Sicher! Meine Antwort folgt einfach dem Standard. Alle Wetten sind deaktiviert, wenn Sie Ihrem Compiler mitteilen, dass er nicht vertraulich ist, insbesondere wenn Sie Fast-Math aktivieren.
kmdreko
11
@Voo Siehe das Zitat in meiner Antwort. Der Wert der RHS wird der Variablen auf der LHS zugewiesen. Es gibt keine rechtliche Rechtfertigung dafür, dass der resultierende Wert der LHS vom Wert der RHS abweicht. Ich schätze, dass einige Compiler diesbezüglich Fehler haben. Aber ob etwas in einem Register gespeichert ist, soll nichts damit zu tun haben.
Leichtigkeitsrennen im Orbit
6
@Voo: In ISO C ++ soll bei jeder Zuweisung auf Typbreite gerundet werden. In den meisten Compilern, die auf x87 abzielen, geschieht dies wirklich nur, wenn der Compiler entscheidet, etwas zu verschütten / neu zu laden. Sie können es gcc -ffloat-storefür strikte Einhaltung erzwingen . Bei dieser Frage geht es jedoch darum, x=y; x==y; dazwischen nichts zu tun. Wenn yes bereits gerundet ist, um in einen Float zu passen, ändert die Konvertierung in Double oder Long Double und Back den Wert nicht. ...
Peter Cordes
34

Ja, ywird sicher den Wert annehmen von x:

[expr.ass]/2: Bei der einfachen Zuweisung (=) wird das Objekt, auf das sich der linke Operand bezieht, geändert ([defns.access]), indem sein Wert durch das Ergebnis des rechten Operanden ersetzt wird.

Es gibt keinen Spielraum für die Zuweisung anderer Werte.

(Andere haben bereits darauf hingewiesen, dass ein Äquivalenzvergleich ==dennoch zu bewerten istfalse NaN-Werte ergibt.)

Das übliche Problem mit Gleitkomma ==ist, dass es leicht ist, nicht ganz den Wert zu haben, den Sie zu tun glauben. Hier wissen wir, dass die beiden Werte, was auch immer sie sind, gleich sind.

Leichtigkeitsrennen im Orbit
quelle
7
@ThomasWeller Das ist ein bekannter Fehler in einer folglich nicht konformen Implementierung. Gut zu erwähnen!
Leichtigkeitsrennen im Orbit
Zuerst dachte ich, dass eine sprachrechtliche Unterscheidung zwischen "Wert" und "Ergebnis" pervers wäre, aber diese Unterscheidung muss nicht ohne Unterschied durch die Sprache von C2.2, 7.1.6 sein; C3.3, 7.1.6; C4.2, 7.1.6 oder C5.3, 7.1.6 des von Ihnen zitierten Standardentwurfs.
Eric Towers
@EricTowers Entschuldigung, können Sie diese Referenzen klären? Ich finde nicht, worauf Sie zeigen
Leichtigkeitsrennen im Orbit
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 und C5.3 .
Eric Towers
@EricTowers Ja, ich folge dir immer noch nicht. Ihr erster Link führt zum Index in Anhang C (sagt mir nichts). Ihre nächsten vier Links gehen alle zu [expr]. Wenn ich die Links ignorieren und mich auf die Zitate konzentrieren möchte, bleibt mir die Verwirrung, dass z. B. C.5.3 die Verwendung des Begriffs "Wert" oder des Begriffs "Ergebnis" nicht zu behandeln scheint (obwohl dies der Fall ist) benutze "result" einmal in seinem normalen englischen Kontext). Vielleicht könnten Sie klarer beschreiben, wo der Standard Ihrer Meinung nach unterscheidet, und ein einziges klares Zitat zu diesem Ereignis liefern. Vielen Dank!
Leichtigkeitsrennen im Orbit
3

Ja, in allen Fällen (ohne Berücksichtigung von NaNs und x87-Problemen) ist dies der Fall.

Wenn Sie eine memcmp Prüfung durchführen, können Sie die Gleichheit testen und NaNs und sNaNs vergleichen. Dazu muss der Compiler auch die Adresse der Variablen übernehmen, wodurch der Wert in einen 32-Bit-Wert floatanstelle eines 80-Bit- Werts umgewandelt wird . Dadurch werden die x87-Probleme behoben. Die zweite Behauptung hier soll nicht zeigen, dass ==NaNs nicht als wahr verglichen werden:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Beachten Sie, dass wenn die NaNs eine andere interne Darstellung haben (dh eine unterschiedliche Mantisse), die memcmpnicht wahr ist.

SS Anne
quelle
1

In normalen Fällen würde es als wahr ausgewertet. (oder die assert-Anweisung macht nichts)

Bearbeiten :

Mit "normalen Fällen" meine ich, dass die oben genannten Szenarien (wie NaN-Werte und 80x87-Gleitkommaeinheiten), wie sie von anderen Benutzern angegeben wurden, ausgeschlossen werden.

Angesichts der Veralterung von 8087-Chips im heutigen Kontext ist das Problem eher isoliert und die Frage, ob sie im aktuellen Zustand der verwendeten Gleitkomma-Architektur anwendbar ist, gilt für alle Fälle mit Ausnahme von NaNs.

(Referenz zu 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Ein großes Lob an @chtz für die Wiedergabe eines guten Beispiels und an @kmdreko für die Erwähnung von NaNs - wusste vorher nichts davon!

Anirban166
quelle
1
Ich dachte, es wäre durchaus möglich x, sich in einem Gleitkommaregister zu befinden, während yes aus dem Speicher geladen wird. Der Speicher ist möglicherweise weniger genau als ein Register, sodass der Vergleich fehlschlägt.
David Schwartz
1
Das könnte ein Fall für eine falsche sein, ich habe nicht so weit gedacht. (Da das OP keine Sonderfälle
vorsah, gehe
1
Ich verstehe nicht wirklich, was du sagst. Wie ich die Frage verstehe, fragt das OP, ob das Kopieren eines Floats und das anschließende Testen auf Gleichheit garantiert erfolgreich sind. Ihre Antwort scheint "Ja" zu sagen. Ich frage, warum die Antwort nicht nein ist.
David Schwartz
6
Die Bearbeitung macht diese Antwort falsch. Der C ++ - Standard verlangt, dass die Zuweisung den Wert in den Zieltyp konvertiert. Bei Ausdrucksauswertungen kann eine übermäßige Genauigkeit verwendet werden, die jedoch möglicherweise nicht durch Zuweisung beibehalten wird. Es ist unerheblich, ob der Wert in einem Register oder in einem Speicher gespeichert ist. Der C ++ - Standard verlangt, dass es sich beim Schreiben des Codes um einen floatWert ohne zusätzliche Genauigkeit handelt.
Eric Postpischil
2
@AProgrammer Angesichts der Tatsache, dass ein (n extrem) fehlerhafter Compiler theoretisch dazu führen könnte, dass int a=1; int b=a; assert( a==b );eine Behauptung ausgelöst wird , halte ich es nur für sinnvoll, diese Frage in Bezug auf einen korrekt funktionierenden Compiler zu beantworten (wobei möglicherweise festgestellt wird, dass einige Versionen einiger Compiler dies tun / haben) -wurde-bekannt-das falsch zu verstehen). In der Praxis, wenn aus irgendeinem Grunde ein Compiler entfernt nicht die zusätzliche Präzision aus dem Ergebnis einer Register gespeicherten Zuordnung, sollte es so tun , bevor es verwendet diesen Wert.
TripeHound
-1

Ja, es wird immer True zurückgegeben , außer wenn es NaN ist . Wenn der Variablenwert NaN ist, wird immer False zurückgegeben !

Valentin Popescu
quelle