TL; DR
- Użyj poniższej funkcji zamiast obecnie przyjętego rozwiązania, aby uniknąć niektórych niepożądanych wyników w pewnych przypadkach granicznych, a jednocześnie być potencjalnie bardziej wydajnym.
- Poznaj oczekiwaną niedokładność swoich liczb i odpowiednio je podawaj w funkcji porównania.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
Grafika, proszę?
Przy porównywaniu liczb zmiennoprzecinkowych istnieją dwa „tryby”.
Pierwszym z nich jest tryb względny , w którym różnica między x
i y
jest rozpatrywana w stosunku do ich amplitudy |x| + |y|
. Przy rysowaniu w 2D daje następujący profil, gdzie kolor zielony oznacza równość x
i y
. (Wziąłem epsilon
0,5 dla celów ilustracyjnych).
Tryb względny jest używany dla „normalnych” lub „dostatecznie dużych” wartości zmiennoprzecinkowych. (Więcej o tym później).
Drugi to tryb absolutny , kiedy po prostu porównujemy ich różnicę do ustalonej liczby. Daje następujący profil (ponownie z epsilon
0,5 i relth
1 dla ilustracji).
Ten absolutny tryb porównania jest używany dla „małych” wartości zmiennoprzecinkowych.
Teraz pytanie brzmi, jak połączyć te dwa wzory odpowiedzi.
W odpowiedzi Michaela Borgwardta przełącznik opiera się na wartości diff
, która powinna być poniżej relth
( Float.MIN_NORMAL
w jego odpowiedzi). Ta strefa przełączania jest pokazana jako zakreskowana na poniższym wykresie.
Ponieważ relth * epsilon
jest mniejszy relth
, zielone plamy nie sklejają się, co z kolei nadaje rozwiązaniu złą właściwość: możemy znaleźć trojaczki liczb takie, x < y_1 < y_2
a jednak x == y2
ale x != y1
.
Weźmy ten uderzający przykład:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
Mamy x < y1 < y2
i faktycznie y2 - x
jest ponad 2000 razy większa niż y1 - x
. A jednak przy obecnym rozwiązaniu
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
Z kolei w rozwiązaniu zaproponowanym powyżej strefa przełączania bazuje na wartości |x| + |y|
, która jest reprezentowana przez zakreskowany kwadrat poniżej. Zapewnia, że obie strefy łączą się wdzięcznie.
Ponadto powyższy kod nie ma rozgałęzień, co mogłoby być bardziej wydajne. Weź pod uwagę, że operacje takie jak max
i abs
, które a priori wymagają rozgałęzienia, często mają dedykowane instrukcje asemblacji. Z tego powodu uważam, że to podejście jest lepsze od innego rozwiązania, które polegałoby na naprawie Michaela nearlyEqual
poprzez zmianę przełącznika z diff < relth
na diff < eps * relth
, co spowodowałoby zasadniczo ten sam wzorzec odpowiedzi.
Gdzie przełączać się między porównaniem względnym i bezwzględnym?
Przełączanie między tymi trybami odbywa się wokół relth
, co jest przyjmowane tak, jak FLT_MIN
w zaakceptowanej odpowiedzi. Ten wybór oznacza, że reprezentacja float32
ogranicza precyzję naszych liczb zmiennoprzecinkowych.
To nie zawsze ma sens. Na przykład, jeśli porównywane liczby są wynikiem odejmowania, być może coś z zakresu FLT_EPSILON
ma większy sens. Jeśli są to pierwiastki kwadratowe z odejmowanych liczb, niedokładność liczbowa może być jeszcze większa.
Jest to raczej oczywiste, gdy rozważasz porównanie liczb zmiennoprzecinkowych z 0
. Tutaj każde względne porównanie zakończy się niepowodzeniem, ponieważ |x - 0| / (|x| + 0) = 1
. Zatem porównanie musi zostać przełączone do trybu bezwzględnego, gdy x
jest na poziomie dokładności obliczeń - i rzadko jest tak niskie, jak FLT_MIN
.
To jest powód wprowadzenia relth
powyższego parametru.
Ponadto, nie mnożąc relth
przez epsilon
, interpretacja tego parametru jest prosta i odpowiada poziomowi dokładności numerycznej, którego oczekujemy od tych liczb.
Matematyczne dudnienie
(trzymane tutaj głównie dla własnej przyjemności)
Bardziej ogólnie zakładam, że dobrze zachowujący się operator porównania zmiennoprzecinkowego =~
powinien mieć kilka podstawowych właściwości.
Oto raczej oczywiste:
- równość siebie:
a =~ a
- symetria:
a =~ b
implikujeb =~ a
- niezmienność przez opozycję:
a =~ b
implikuje-a =~ -b
(Nie mamy a =~ b
i b =~ c
sugeruje a =~ c
, że =~
nie jest to relacja równoważności).
Dodałbym następujące właściwości, które są bardziej specyficzne dla porównań zmiennoprzecinkowych
- jeśli
a < b < c
, to a =~ c
implikuje a =~ b
(bliższe wartości również powinny być równe)
- jeśli
a, b, m >= 0
to a =~ b
oznacza a + m =~ b + m
(większe wartości z tą samą różnicą również powinny być równe)
- jeśli
0 <= λ < 1
to a =~ b
oznacza λa =~ λb
(być może mniej oczywiste do argumentowania za).
Te właściwości już dają silne ograniczenia dla możliwych funkcji bliskich równości. Weryfikuje je funkcja zaproponowana powyżej. Być może brakuje jednej lub kilku innych oczywistych właściwości.
Kiedy myślimy o =~
rodzinie relacji równości =~[Ɛ,t]
sparametryzowanej przez Ɛ
i relth
, można również dodać
- jeśli
Ɛ1 < Ɛ2
to a =~[Ɛ1,t] b
implikuje a =~[Ɛ2,t] b
(równość dla danej tolerancji oznacza równość przy wyższej tolerancji)
- jeśli
t1 < t2
to a =~[Ɛ,t1] b
implikuje a =~[Ɛ,t2] b
(równość dla danej niedokładności implikuje równość przy większej nieprecyzyjności)
Zaproponowane rozwiązanie również je weryfikuje.