Jeśli skopiuję zmiennoprzecinkową do innej zmiennej, czy będą one równe?

167

Wiem, że ==sprawdzanie równości zmiennych zmiennoprzecinkowych nie jest dobrym sposobem. Ale chcę to wiedzieć za pomocą następujących stwierdzeń:

float x = ...

float y = x;

assert(y == x)

Skoro yjest on kopiowany x, czy twierdzenie to będzie prawdziwe?

Wei Li
źródło
78
Daję nagrodę w wysokości 50 osobom, które faktycznie udowodnią nierówność poprzez demonstrację z prawdziwym kodem. Chcę zobaczyć grę 80 vs 64 bit w akcji. Plus kolejne 50 za wyjaśnienie wygenerowanego kodu asemblera, który pokazuje, że jedna zmienna znajduje się w rejestrze, a druga nie (lub niezależnie od przyczyny nierówności, chciałbym, żeby była wyjaśniona na niskim poziomie).
Thomas Weller
1
@ThomasWeller błąd GCC na ten temat: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; Jednak właśnie próbowałem odpalić go na systemie x86-64 i nie robi tego nawet z -ffast-matematyki. Podejrzewam, że potrzebujesz starego GCC w systemie 32-bitowym.
pjc50
5
@ pjc50: Właściwie potrzebujesz 80-bitowego systemu do odtworzenia błędu 323; to FPU 80x87 spowodowało problem. x86-64 używa SSE FPU. Dodatkowe bity powodują problem, ponieważ są zaokrąglane podczas rozlewania wartości do liczby zmiennoprzecinkowej 32-bitowej.
MSalters
4
Jeśli teoria MSalters jest poprawna (i podejrzewam, że tak jest), możesz wykonać repro albo kompilując dla 32-bitów ( -m32), albo poinstruując GCC, aby użyła FPU x87 ( -mfpmath=387).
Cody Gray
4
Zmień „48 bit” na „80 bit”, a następnie usuń tam „mityczny” przymiotnik @Hot. Właśnie to było omawiane bezpośrednio przed twoim komentarzem. X87 (FPU dla architektury x86) korzysta z rejestrów 80-bitowych, formatu „o rozszerzonej precyzji”.
Cody Gray

Odpowiedzi:

125

Oprócz assert(NaN==NaN);przypadku wskazanego przez kmdreko, możesz mieć sytuacje z matematyką x87, kiedy 80-bitowe zmiennoprzecinkowe są tymczasowo przechowywane w pamięci, a później porównywane z wartościami, które są nadal przechowywane w rejestrze.

Możliwy minimalny przykład, który kończy się niepowodzeniem z gcc9.2 po kompilacji z -O2 -m32:

#include <cassert>

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

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

volatilePrawdopodobnie mogą być pominięte, jeśli uda się stworzyć wystarczające ciśnienie do rejestru są yprzechowywane i przeładowywane z pamięci (ale mylić wystarczająco kompilatora, aby nie pominąć porównania wszystko razem).

Zobacz dokumentację GCC FAQ:

chtz
źródło
2
Wydaje się dziwne, że dodatkowe bity byłyby brane pod uwagę przy porównywaniu floatstandardowej precyzji z dodatkową precyzją.
Nat
13
@Nat To jest dziwne; to jest błąd .
Wyścigi lekkości na orbicie
13
@ThomasWeller Nie, to rozsądna nagroda. Chociaż chciałbym, aby odpowiedź zwróciła uwagę, że jest to zachowanie niezgodne
Lightness Races in Orbit
4
Mogę rozszerzyć tę odpowiedź, wskazując, co dokładnie dzieje się w kodzie asemblera, i że to faktycznie narusza standard - chociaż nie nazwałbym siebie prawnikiem językowym, więc nie mogę zagwarantować, że nie jest niejasny klauzula, która wyraźnie zezwala na takie zachowanie. Zakładam, że OP był bardziej zainteresowany praktycznymi komplikacjami na rzeczywistych kompilatorach, a nie na całkowicie wolnych od błędów, w pełni zgodnych kompilatorach (które, jak sądzę, de facto nie istnieją).
chtz
4
Warto wspomnieć, że -ffloat-storewydaje się, że jest to sposób, aby temu zapobiec.
OrangeDog
116

Nie jest to prawdą, jeśli tak xjest NaN, ponieważ porównania NaNzawsze fałszywe (tak, nawet NaN == NaN). Dla wszystkich innych przypadków (wartości normalne, wartości nienormalne, nieskończoności, zera) to twierdzenie będzie prawdziwe.

Porada dotycząca unikania liczb zmiennoprzecinkowych ==dotyczy obliczeń, ponieważ liczby zmiennoprzecinkowe nie są w stanie wyrazić wielu wyników dokładnie wtedy, gdy są używane w wyrażeniach arytmetycznych. Przypisanie nie jest obliczeniem i nie ma powodu, aby przypisanie przyniosło inną wartość niż oryginał.


Ocena o zwiększonej precyzji nie powinna stanowić problemu, jeśli przestrzegany jest standard. Z <cfloat>odziedziczony po C [5.2.4.2.2.8] ( moje podkreślenie ):

Z wyjątkiem przypisania i rzutowania (które usuwają cały dodatkowy zakres i precyzję) , wartości operacji z operandami zmiennoprzecinkowymi oraz wartości podlegające zwykłym konwersjom arytmetycznym i zmiennym zmiennym są oceniane w formacie, którego zakres i precyzja mogą być większe niż wymagane przez rodzaj.

Jednak, jak wskazano w komentarzach, niektóre przypadki z niektórymi kompilatorami, opcjami kompilacji i celami mogą sprawić, że paradoksalnie będzie to fałszywe.

kmdreko
źródło
10
Co jeśli xzostanie obliczony w rejestrze w pierwszym wierszu, zachowując większą dokładność niż minimum dla float. y = xMoże znajdować się w pamięci, zachowując jedynie floatprecyzji. Następnie test równości zostałby wykonany z pamięcią względem rejestru, z różnymi dokładnościami, a zatem bez gwarancji.
David Schwartz
5
x+pow(b,2)==x+pow(a,3)może się różnić od tego, auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoże można porównać przy użyciu większej precyzji niż drugi (jeśli jeden / dwa są 64-bitowymi wartościami w ram, podczas gdy wartości intermediste są 80-bitowe na fpu). Czasami zadanie może coś zrobić.
Yakk - Adam Nevraumont
22
@evg Sure! Moja odpowiedź jest po prostu zgodna ze standardem. Wszystkie zakłady są wyłączone, jeśli powiesz swojemu kompilatorowi, by nie wysyłał błędów, szczególnie gdy włączasz szybką matematykę.
kmdreko
11
@ Voo Zobacz cytat w mojej odpowiedzi. Wartość RHS jest przypisana do zmiennej w LHS. Nie ma prawnego uzasadnienia, aby wynikowa wartość LHS różniła się od wartości RHS. Rozumiem, że kilka kompilatorów zawiera błędy w tym zakresie. Ale to, czy coś jest przechowywane w rejestrze, nie powinno mieć z tym nic wspólnego.
Wyścigi lekkości na orbicie
6
@Voo: W ISO C ++ zaokrąglanie do szerokości tekstu powinno mieć miejsce w każdym zadaniu. W większości kompilatorów, których celem jest x87, dzieje się tak naprawdę tylko wtedy, gdy kompilator zdecyduje się wylać / przeładować. Możesz wymusić to gcc -ffloat-storedla ścisłej zgodności. Ale to pytanie dotyczy x=y; x==y; tego, aby nie robić niczego, aby się między nimi różnić. Jeśli yjest już zaokrąglony, aby pasował do liczby zmiennoprzecinkowej, konwersja na podwójne lub długie podwójne i odwrotne nie zmieni wartości. ...
Peter Cordes
34

Tak, na ypewno przyjmie wartość x:

[expr.ass]/2: W prostym przypisaniu (=) obiekt, do którego odwołuje się lewy operand, jest modyfikowany ([defns.access]) poprzez zamianę jego wartości na wynik prawego operandu.

Nie ma swobody w przypisywaniu innych wartości.

(Inni już wskazali, że porównanie równoważności ==będzie jednak oceniać falsedla wartości NaN).

Zwykłym problemem w przypadku liczb zmiennoprzecinkowych ==jest to, że łatwo nie mieć takiej wartości, jak myślisz. Tutaj wiemy, że dwie wartości, bez względu na to, jakie są, są takie same.

Lekkość Wyścigi na orbicie
źródło
7
@ThomasWeller To znany błąd w konsekwentnie niezgodnej implementacji. Warto jednak o tym wspomnieć!
Wyścigi lekkości na orbicie
Na początku myślałem, że język prawny dla rozróżnienia między „wartością” a „wynikiem” byłby przewrotny, ale to rozróżnienie nie musi być bez różnicy dla języka C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 lub C5.3, 7.1.6 projektu normy, którą przytaczasz.
Eric Towers
@EricTowers Przepraszamy, możesz wyjaśnić te referencje? Nie znajduję tego, na co wskazujesz
Lekkość ściga się na orbicie
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 i C5.3 .
Eric Towers
@EricTowers Tak, nadal cię nie śledzę. Twój pierwszy link prowadzi do indeksu dodatku C (nic mi nie mówi). Wszystkie następne cztery linki prowadzą do [expr]. Jeśli mam zignorować linki i skupić się na cytatach, to mam zamieszanie, że np. C.5.3 nie wydaje się dotyczyć użycia terminu „wartość” lub terminu „wynik” (chociaż tak jest użyj „wynik” raz w normalnym angielskim języku). Być może mógłbyś bardziej precyzyjnie opisać, gdzie według ciebie standard wprowadza rozróżnienie, i podać jeden wyraźny cytat z tego wydarzenia. Dzięki!
Wyścigi lekkości na orbicie
3

Tak, we wszystkich przypadkach (pomijając problemy z NaN i x87), będzie to prawdą.

Jeśli to zrobisz memcmp, będziesz w stanie sprawdzić równość, jednocześnie porównując NaN i sNaN. Będzie to również wymagało od kompilatora pobrania adresu zmiennej, która zmusi wartość do 32-bitowej floatzamiast 80-bitowej. To wyeliminuje problemy z x87. Drugie stwierdzenie tutaj nie ma na celu wykazania, że ==nie porównuje NaN jako prawdy:

#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;
}

Zauważ, że jeśli NaN mają inną reprezentację wewnętrzną (tj. Różną mantysę), memcmpto nie będzie porównywać prawdy.

SS Anne
źródło
1

W zwykłych przypadkach byłoby to prawdą. (lub instrukcja assert nic nie zrobi)

Edytuj :

Przez „zwykłe przypadki” mam na myśli wykluczenie wyżej wymienionych scenariuszy (takich jak wartości NaN i jednostki zmiennoprzecinkowe 80x87) wskazanych przez innych użytkowników.

Biorąc pod uwagę przestarzałość 8087 układów w dzisiejszym kontekście, problem jest raczej odizolowany, a pytanie ma zastosowanie w obecnym stanie architektury zmiennoprzecinkowej, jest prawdziwe we wszystkich przypadkach z wyjątkiem NaN.

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

Uznanie dla @chtz za odtworzenie dobrego przykładu i @kmdreko za wzmiankę o NaNs - wcześniej o nich nie wiedziałem!

Anirban166
źródło
1
Myślałem, że jest całkowicie możliwe, xaby być w rejestrze zmiennoprzecinkowym, gdy yjest ładowany z pamięci. Pamięć może mieć mniejszą dokładność niż rejestr, co powoduje niepowodzenie porównania.
David Schwartz
1
To może być jeden przypadek fałszu, nie myślałem tak daleko. (ponieważ PO nie przedstawił żadnych specjalnych przypadków, nie
zakładam
1
Naprawdę nie rozumiem, co mówisz. Jak rozumiem pytanie, OP pyta, czy kopiowanie liczby zmiennoprzecinkowej, a następnie zagwarantowanie powodzenia testów równości jest gwarantowane. Twoja odpowiedź wydaje się brzmieć „tak”. Pytam, dlaczego odpowiedź nie brzmi „nie”.
David Schwartz
6
Edycja powoduje, że ta odpowiedź jest niepoprawna. Standard C ++ wymaga, aby przypisanie przekształciło wartość na typ docelowy - nadmierna precyzja może być użyta w ocenach wyrażeń, ale nie może zostać zachowana poprzez przypisanie. Nie ma znaczenia, czy wartość jest przechowywana w rejestrze, czy w pamięci; standard C ++ wymaga, aby podczas pisania kodu była to floatwartość bez dodatkowej precyzji.
Eric Postpischil
2
@AProgrammer Biorąc pod uwagę, że (n skrajnie) błędny kompilator może teoretycznie spowodować int a=1; int b=a; assert( a==b );wyrzucenie asercji, myślę, że sensowne jest udzielenie odpowiedzi na to pytanie tylko w odniesieniu do poprawnie działającego kompilatora (jednocześnie zauważając, że niektóre wersje niektórych kompilatorów mają / mają -been-wiadomo-to zrobić źle). W praktyce, jeśli z jakiegoś powodu kompilator nie usunie dodatkowej precyzji z wyniku przypisania do rejestru, powinien to zrobić, zanim użyje tej wartości.
TripeHound
-1

Tak, zawsze będzie zwracać wartość True , chyba że jest to NaN . Jeśli wartość zmiennej to NaN , zawsze zwraca False !

Valentin Popescu
źródło