Czy sprawdzanie wartości zmiennoprzecinkowych pod kątem równości do 0 jest bezpieczne?


100

Wiem, że normalnie nie można polegać na równości między wartościami typu podwójnego lub dziesiętnego, ale zastanawiam się, czy 0 to przypadek specjalny.

Chociaż rozumiem niedokładności między 0,00000000000001 a 0,00000000000002, samo 0 wydaje się dość trudne do zepsucia, ponieważ to po prostu nic. Jeśli nic nie jesteś dokładny, to już nie jest nic.

Ale nie wiem zbyt wiele na ten temat, więc nie mogę o tym mówić.

double x = 0.0;
return (x == 0.0) ? true : false;

Czy to zawsze będzie prawdą?


69
Operator trójskładnikowy jest zbędny w tym kodzie :)
Joel Coehoorn

5
LOL masz rację. Go me
Gene Roberts

Nie zrobiłbym tego, ponieważ nie wiesz, jak x zostało ustawione na zero. Jeśli nadal chcesz to zrobić, prawdopodobnie chcesz zaokrąglić lub zaokrąglić x piętro, aby pozbyć się 1e-12 lub czegoś takiego, który może być oznaczony na końcu.
Rex Logan

Odpowiedzi:


115

Można bezpiecznie oczekiwać, że porównanie zwróci truewtedy i tylko wtedy, gdy podwójna zmienna ma wartość dokładnie 0.0(co oczywiście ma miejsce w oryginalnym fragmencie kodu). Jest to zgodne z semantyką ==operatora. a == boznacza „ arówna się b”.

Nie jest bezpieczne (ponieważ nie jest poprawne ) oczekiwanie, że wynik niektórych obliczeń będzie wynosił zero w arytmetyce podwójnej (lub bardziej ogólnie, zmiennoprzecinkowej), ilekroć wynik tego samego obliczenia w czystej matematyce wynosi zero. Dzieje się tak, ponieważ gdy obliczenia zaczynają się pojawiać, pojawia się błąd precyzji zmiennoprzecinkowej - pojęcie, które nie istnieje w arytmetyce liczb rzeczywistych w matematyce.


51

Jeśli potrzebujesz wykonać wiele porównań „równości”, dobrym pomysłem może być napisanie małej funkcji pomocniczej lub metody rozszerzającej w .NET 3.5 do porównania:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Można to wykorzystać w następujący sposób:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);

4
Możesz mieć odejmowany błąd anulowania, porównując double1 i double2, na wypadek, gdyby te liczby miały wartości bardzo zbliżone do siebie. Usunąłbym Math.Abs ​​i sprawdziłbym każdą gałąź osobno d1> = d2 - e i d1 <= d2 + e
Theodore Zographos

„Ponieważ Epsilon definiuje minimalne wyrażenie dodatniej wartości, której zakres jest bliski zeru, margines różnicy między dwiema podobnymi wartościami musi być większy niż Epsilon. Zazwyczaj jest on wielokrotnie większy niż Epsilon. Z tego powodu zalecamy nie używaj Epsilon podczas porównywania wartości Double dla równości. " - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Rafael Costa

15

Dla twojej prostej próbki ten test jest w porządku. Ale co z tym:

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Pamiętaj, że .1 to powtarzający się dziesiętny w systemie dwójkowym i nie może być dokładnie reprezentowany. Następnie porównaj to z tym kodem:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Zostawię ci przeprowadzenie testu, aby zobaczyć rzeczywiste wyniki: bardziej prawdopodobne jest, że zapamiętasz to w ten sposób.


5
W rzeczywistości zwraca to true z jakiegoś powodu (przynajmniej w LINQPad).
Alexey Romanov

O czym „problem .1” mówisz?
Teejay

14

Z wpisu MSDN dla Double.Equals :

Precyzja w porównaniach

Z metody Equals należy korzystać ostrożnie, ponieważ dwie pozornie równoważne wartości mogą być nierówne ze względu na różną precyzję tych dwóch wartości. Poniższy przykład zgłasza, że ​​Double wartość .3333 i Double zwrócone przez podzielenie 1 przez 3 są nierówne.

...

Zamiast porównywania pod kątem równości, jedna zalecana technika polega na zdefiniowaniu dopuszczalnego marginesu różnicy między dwiema wartościami (np. 0,01% jednej z wartości). Jeżeli bezwzględna wartość różnicy między dwiema wartościami jest mniejsza lub równa temu marginesowi, różnica prawdopodobnie wynika z różnic w precyzji, a zatem wartości prawdopodobnie będą równe. W poniższym przykładzie zastosowano tę technikę do porównania .33333 i 1/3, dwóch Double wartości, które w poprzednim przykładzie kodu były nierówne.

Zobacz także Double.Epsilon .


1
Możliwe jest również porównanie wartości niezupełnie równoważnych jako równe. Można by się spodziewać, że jeśli x.Equals(y), wtedy (1/x).Equals(1/y), ale tak nie xjest, jeśli jest 0i yjest 1/Double.NegativeInfinity. Wartości te są deklarowane jako równe, chociaż ich odwrotności nie.
supercat

@supercat: są równoważne. I nie mają wzajemności. Możesz ponownie uruchomić test za pomocą x = 0i y = 0, i nadal to znajdziesz 1/x != 1/y.
Ben Voigt,

@BenVoigt: Z xi yjako typ double? Jak porównujecie wyniki, aby zgłaszać nierówności? Zauważ, że 1 / 0,0 nie jest NaN.
supercat

@supercat: Ok, to jedna z rzeczy, w których IEEE-754 się myli. (Po pierwsze, 1.0/0.0nie jest to NaN tak, jak powinno, ponieważ granica nie jest wyjątkowa. Po drugie, że nieskończoności porównują się równo ze sobą, bez zwracania uwagi na stopnie nieskończoności)
Ben Voigt

@BenVoigt: Jeśli zero było wynikiem pomnożenia dwóch bardzo małych liczb, to podzielenie 1,0 przez to powinno dać wartość, która jest większa niż dowolna liczba małych liczb, które mają ten sam znak i mniejszą niż dowolna liczba, jeśli jedna z małych numery miały przeciwne znaki. IMHO, IEEE-754 byłby lepszy, gdyby miał zero bez znaku, ale dodatnie i ujemne nieskończenie małe.
supercat,

6

Problem pojawia się, gdy porównujesz różne typy implementacji wartości zmiennoprzecinkowych, np. Porównując zmiennoprzecinkowe z double. Ale z tym samym typem nie powinno to stanowić problemu.

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

Problem w tym, że programista czasami zapomina, że ​​niejawne rzutowanie typu (double na float) ma miejsce dla porównania i skutkuje to błędem.


3

Jeśli liczba została bezpośrednio przypisana do liczby zmiennoprzecinkowej lub podwójnej, można bezpiecznie przetestować względem zera lub dowolnej liczby całkowitej, którą można przedstawić za pomocą 53 bitów dla liczby podwójnej lub 24 bitów dla liczby zmiennoprzecinkowej.

Inaczej mówiąc, zawsze możesz przypisać podwójną wartość i wartość całkowitą, a następnie porównać podwójną wartość z powrotem do tej samej liczby całkowitej i mieć pewność, że będzie równa.

Możesz również zacząć od przypisania liczby całkowitej i wykonywać proste porównania, kontynuując pracę, trzymając się dodawania, odejmowania lub mnożenia przez liczby całkowite (zakładając, że wynik jest mniejszy niż 24 bity dla liczby zmiennoprzecinkowej i 53 bity dla podwójnej). Możesz więc traktować liczby zmiennoprzecinkowe i podwojenia jako liczby całkowite w pewnych kontrolowanych warunkach.


Ogólnie zgadzam się z twoim stwierdzeniem (i przegłosowałem je), ale uważam, że to naprawdę zależy od tego, czy implementacja zmiennoprzecinkowa IEEE 754 jest używana, czy nie. I uważam, że każdy „nowoczesny” komputer używa IEEE 754, przynajmniej do przechowywania danych zmiennoprzecinkowych (istnieją dziwne zasady zaokrąglania, które się różnią).
Mark Lakata

2

Nie, to nie jest w porządku. Tak zwane zdenormalizowane wartości (subnormal), w porównaniu do 0,0, byłyby porównywane jako fałszywe (niezerowe), ale użyte w równaniu byłyby znormalizowane (stały się 0,0). Dlatego używanie tego jako mechanizmu unikania dzielenia przez zero nie jest bezpieczne. Zamiast tego dodaj 1.0 i porównaj z 1.0. Zapewni to traktowanie wszystkich wartości podrzędnych jako zero.


Subnormals są również znane jako denormals
Manuel

Wartości podnormalne nie stają się równe zeru, gdy są używane, chociaż mogą, ale nie muszą, dawać ten sam wynik w zależności od dokładnej operacji.
mądry

-2

Spróbuj tego, a przekonasz się, że == nie jest wiarygodne dla double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Oto odpowiedź od firmy Quora.


-4

Właściwie myślę, że lepiej jest użyć następujących kodów, aby porównać podwójną wartość z 0,0:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

To samo dotyczy float:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;

5
Nie. Z dokumentacji double.Epsilon: „Jeśli utworzysz niestandardowy algorytm, który określa, czy dwie liczby zmiennoprzecinkowe można uznać za równe, musisz użyć wartości większej niż stała Epsilon, aby ustalić dopuszczalny bezwzględny margines różnicy aby te dwie wartości były uważane za równe. (Zazwyczaj ten margines różnicy jest wielokrotnie większy niż Epsilon.) "
Alastair Maw

1
@AlastairMaw odnosi się to do sprawdzania równości dwóch podwójnych dowolnego rozmiaru. Aby sprawdzić równość do zera, należy użyć double. Epsilon jest w porządku.
jwg,

4
Nie, to nie . Jest bardzo prawdopodobne, że wartość, do której doszedłeś za pomocą niektórych obliczeń, jest wielokrotnie oddalona od zera o epsilon, ale nadal powinna być uważana za zero. Nie osiągasz w magiczny sposób całej masy dodatkowej precyzji w swoim pośrednim wyniku skądś, tylko dlatego, że zdarza się, że jest bliski zeru.
Alastair Maw,

4
Na przykład: (1,0 / 5,0 + 1,0 / 5,0 - 1,0 / 10,0 - 1,0 / 10,0 - 1,0 / 10,0 - 1,0 / 10,0) <double.Epsilon == false (i znacznie tak pod względem wielkości: 2,78E-17 vs 4,94E -324)
Alastair Maw

więc jaka jest zalecana dokładność, jeśli double.Epsilon nie jest w porządku? Czy 10 razy epsilon byłby w porządku? 100 razy?
liang
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.