Przede wszystkim wartości zmiennoprzecinkowe nie są „losowe” w swoim zachowaniu. Dokładne porównanie może i ma sens w wielu rzeczywistych zastosowaniach. Ale jeśli zamierzasz użyć zmiennoprzecinkowego, musisz wiedzieć, jak to działa. Błąd polegający na założeniu, że zmiennoprzecinkowe działa jak liczby rzeczywiste, spowoduje, że kod szybko się zepsuje. Błąd polegający na założeniu, że wyniki zmiennoprzecinkowe są powiązane z dużym losowym rozmyciem (jak sugeruje większość odpowiedzi tutaj), otrzyma kod, który wydaje się działać na początku, ale ostatecznie zawiera błędy dużej wielkości i niedziałające przypadki narożników.
Przede wszystkim, jeśli chcesz programować z liczbą zmiennoprzecinkową, powinieneś przeczytać to:
Co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej
Tak, przeczytaj wszystko. Jeśli jest to zbyt duże obciążenie, do obliczeń należy używać liczb całkowitych / ustalonego punktu, dopóki nie będzie czasu, aby je odczytać. :-)
To powiedziawszy, największe problemy z dokładnymi porównaniami zmiennoprzecinkowymi sprowadzają się do:
Fakt, że wiele wartości, które możesz zapisać w źródle lub wczytać za pomocą scanf
lub strtod
, nie istnieje jako wartości zmiennoprzecinkowe i po cichu są konwertowane na najbliższe przybliżenie. O tym mówiła odpowiedź demon9733.
Fakt, że wiele wyników jest zaokrąglanych z powodu braku wystarczającej dokładności do przedstawienia rzeczywistego wyniku. Prostym przykładem, w którym można to zobaczyć, jest dodawanie x = 0x1fffffe
i y = 1
jako zmiennoprzecinkowe. Tutaj x
ma 24 bity precyzji w mantysie (ok) i y
ma tylko 1 bit, ale kiedy je dodasz, ich bity nie nakładają się na siebie, a wynik wymagałby 25 bitów precyzji. Zamiast tego zostaje zaokrąglony ( 0x2000000
w domyślnym trybie zaokrąglania).
Fakt, że wiele wyników jest zaokrąglanych z powodu nieskończonej liczby miejsc dla prawidłowej wartości. Obejmuje to zarówno racjonalne wyniki, takie jak 1/3 (które znasz od miejsc po przecinku, gdzie zajmuje nieskończenie wiele miejsc), ale także 1/10 (co również zajmuje nieskończenie wiele miejsc w systemie binarnym, ponieważ 5 nie jest potęgą 2), a także irracjonalne wyniki, takie jak pierwiastek kwadratowy z czegoś, co nie jest idealnym kwadratem.
Podwójne zaokrąglenie. W niektórych systemach (szczególnie x86) wyrażenia zmiennoprzecinkowe są oceniane z większą precyzją niż ich typy nominalne. Oznacza to, że gdy nastąpi jeden z powyższych rodzajów zaokrąglania, otrzymasz dwa kroki zaokrąglania, najpierw zaokrąglenie wyniku do typu o wyższej precyzji, a następnie zaokrąglenie do typu końcowego. Jako przykład rozważmy, co dzieje się w systemie dziesiętnym, jeśli zaokrąglisz 1,49 do liczby całkowitej (1), w porównaniu do tego, co się stanie, jeśli najpierw zaokrąglisz go do jednego miejsca po przecinku (1,5), a następnie zaokrąglisz ten wynik do liczby całkowitej (2). Jest to w rzeczywistości jeden z najgorszych obszarów, z którymi należy się zmierzyć w liczbach zmiennoprzecinkowych, ponieważ zachowanie kompilatora (szczególnie w przypadku błędnych, niezgodnych kompilatorów, takich jak GCC) jest nieprzewidywalne.
Funkcje transcendentalne ( trig
, exp
, log
, itd.) Nie są podane na poprawne zaokrąglone wyniki; wynik jest po prostu określony jako poprawny w obrębie jednej jednostki w ostatnim miejscu precyzji (zwykle określanym jako 1ulp ).
Pisząc kod zmiennoprzecinkowy, należy pamiętać o tym, co robisz z liczbami, które mogą powodować niedokładność wyników, i odpowiednio dokonywać porównań. Często ma sens porównywanie z „epsilonem”, ale ten epsilon powinien opierać się na wielkości porównywanych liczb , a nie na absolutnej stałej. (W przypadkach, w których działałaby absolutna stała epsilon, jest to silnie wskazujące, że punkt stały, a nie zmiennoprzecinkowy, jest właściwym narzędziem do pracy!)
Edycja: W szczególności sprawdzenie epsilon w zależności od wielkości powinno wyglądać mniej więcej tak:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))
Gdzie FLT_EPSILON
jest stała od float.h
(wymienić go DBL_EPSILON
na double
s lub LDBL_EPSILON
dla long double
S) i K
jest stałą wybrać takie, że skumulowany błąd swoich obliczeniach jest zdecydowanie ograniczona przez K
jednostki na ostatnim miejscu (a jeśli nie jesteś pewien, że masz błąd powiązane obliczenia, zrób K
kilka razy większe niż to, co mówią twoje obliczenia).
Na koniec zauważ, że jeśli go użyjesz, może być wymagana szczególna ostrożność w pobliżu zera, ponieważ FLT_EPSILON
nie ma to sensu w przypadku denormali. Szybkim rozwiązaniem byłoby, aby:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)
i podobnie zamień, DBL_MIN
jeśli używasz podwójnych.
fabs(x+y)
jest problematyczne, jeślix
iy
(może) mieć inny znak. Mimo to dobra odpowiedź na falę porównań kultu ładunku.