Jak niebezpieczne jest porównywanie wartości zmiennoprzecinkowych?


391

Znam UIKitzastosowania z CGFloatpowodu niezależnego od rozdzielczości układu współrzędnych.

Ale za każdym razem, gdy chcemy sprawdzić, czy na przykład frame.origin.xjest 0to sprawia, że czuję się chory:

if (theView.frame.origin.x == 0) {
    // do important operation
}

Nie jest CGFloatpodatny na fałszywych alarmów przy porównywaniu z ==, <=, >=, <, >? Jest to zmiennoprzecinkowy i mają problemy z precyzją: 0.0000000000041na przykład.

Czy jest Objective-Cto obsługiwane wewnętrznie podczas porównywania, czy może zdarza się, że origin.xodczyt o wartości zero nie jest porównywany z wartością 0prawdziwą?

Odpowiedzi:


466

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:

  1. Fakt, że wiele wartości, które możesz zapisać w źródle lub wczytać za pomocą scanflub strtod, nie istnieje jako wartości zmiennoprzecinkowe i po cichu są konwertowane na najbliższe przybliżenie. O tym mówiła odpowiedź demon9733.

  2. 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 = 0x1fffffei y = 1jako zmiennoprzecinkowe. Tutaj xma 24 bity precyzji w mantysie (ok) i yma 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 ( 0x2000000w domyślnym trybie zaokrąglania).

  3. 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.

  4. 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.

  5. 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_EPSILONjest stała od float.h(wymienić go DBL_EPSILONna doubles lub LDBL_EPSILONdla long doubleS) i Kjest stałą wybrać takie, że skumulowany błąd swoich obliczeniach jest zdecydowanie ograniczona przez Kjednostki na ostatnim miejscu (a jeśli nie jesteś pewien, że masz błąd powiązane obliczenia, zrób Kkilka 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_EPSILONnie 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_MINjeśli używasz podwójnych.


25
fabs(x+y)jest problematyczne, jeśli xi y(może) mieć inny znak. Mimo to dobra odpowiedź na falę porównań kultu ładunku.
Daniel Fischer

27
Jeśli xi ymają inny znak, nie ma problemu. Z prawej strony będzie „zbyt mały”, ale ponieważ xi ymieć inny znak, nie należy porównać równe w każdym razie. (Chyba, że ​​są tak małe, że są denormalne, ale wtedy druga sprawa je łapie)
R .. GitHub ZATRZYMAJ LODĘ

4
Jestem ciekawy twojego stwierdzenia: „szczególnie dla błędnych, niezgodnych kompilatorów, takich jak GCC”. Czy naprawdę GCC jest wadliwy, a także niezgodny?
Nicolás Ozimica,

3
Ponieważ pytanie jest oznaczone iOS, warto zauważyć, że kompilatory Apple (zarówno kompilacja clang, jak i gcc Apple) zawsze używały FLT_EVAL_METHOD = 0, i starają się być całkowicie surowi, aby nie wykazywać nadmiernej precyzji. Jeśli znajdziesz jakieś naruszenie tego, zgłoś zgłoszenia błędów.
Stephen Canon

17
„Przede wszystkim wartości zmiennoprzecinkowe nie są„ losowe ”w swoim zachowaniu. Dokładne porównanie może i ma sens w wielu zastosowaniach w świecie rzeczywistym.” - Tylko dwa zdania i już zdobył +1! To jedno z najbardziej niepokojących nieporozumień, jakie ludzie popełniają podczas pracy z zmiennoprzecinkowymi punktami.
Christian Rau,

36

Ponieważ 0 jest dokładnie reprezentowalne jako liczba zmiennoprzecinkowa IEEE754 (lub przy użyciu dowolnej innej implementacji liczb fp, z którymi kiedykolwiek pracowałem), porównanie z 0 jest prawdopodobnie bezpieczne. Możesz zostać ugryziony, jeśli twój program wyliczy (na przykład theView.frame.origin.x) wartość, która według ciebie powinna wynosić 0, ale której obliczenie nie może zagwarantować 0.

Aby wyjaśnić trochę, obliczenia takie jak:

areal = 0.0

(chyba że Twój język lub system jest uszkodzony) utworzy taką wartość, że (areal == 0.0) zwróci true, ale inne obliczenia, takie jak

areal = 1.386 - 2.1*(0.66)

może nie.

Jeśli możesz się upewnić, że twoje obliczenia dają wartości 0 (a nie tylko to, że powinny one wynosić 0), możesz przejść dalej i porównać wartości fp z 0. Jeśli nie możesz się upewnić w wymaganym stopniu , najlepiej trzymać się zwykłego podejścia „równości tolerowanej”.

W najgorszych przypadkach nieostrożne porównanie wartości fp może być bardzo niebezpieczne: pomyśl awionikę, prowadzenie broni, operacje w elektrowniach, nawigację w pojazdach, prawie każdą aplikację, w której obliczenia spełniają rzeczywisty świat.

Dla Angry Birds nie jest tak niebezpieczne.


11
W rzeczywistości 1.30 - 2*(0.65)jest doskonałym przykładem wyrażenia, które ewaluuje do wartości 0,0, jeśli twój kompilator implementuje IEEE 754, ponieważ liczby podwójne reprezentowane jako 0.65i 1.30mają takie same znaczenia, a mnożenie przez dwa jest oczywiście dokładne.
Pascal Cuoq,

7
Nadal otrzymuję rep z tego, więc zmieniłem drugi przykładowy fragment.
Znak wysokiej wydajności

22

Chcę dać nieco inną odpowiedź niż inne. Świetnie nadają się do odpowiedzi na zadane pytanie, ale prawdopodobnie nie do tego, co musisz wiedzieć lub jaki jest twój prawdziwy problem.

Zmienny punkt w grafice jest w porządku! Ale prawie nie ma potrzeby nigdy porównywać pływaków bezpośrednio. Dlaczego miałbyś to robić? Grafika używa pływaków do definiowania interwałów. Porównywanie, czy liczba zmiennoprzecinkowa mieści się w przedziale zdefiniowanym również przez zmiennoprzecinkową, jest zawsze dobrze zdefiniowana i wymaga jedynie spójności, niedokładności lub precyzji! Tak długo, jak piksel (który jest również interwałem!) Może być przypisany, to wszystko potrzebuje grafiki.

Więc jeśli chcesz sprawdzić, czy twój punkt znajduje się poza zakresem [0.. szerokości [to jest w porządku. Upewnij się tylko, że konsekwentnie definiujesz włączenie. Na przykład zawsze określ, że wewnątrz jest (x> = 0 i& x <szerokość). To samo dotyczy testów skrzyżowań lub trafień.

Jeśli jednak nadużywasz współrzędnych grafiki jako jakiegoś rodzaju flagi, np. Aby sprawdzić, czy okno jest zadokowane, czy nie, nie powinieneś tego robić. Zamiast tego użyj flagi boolowskiej, która jest oddzielna od warstwy prezentacji graficznej.


13

Porównywanie do zera może być bezpieczną operacją, o ile zero nie było wartością obliczoną (jak zauważono w powyższej odpowiedzi). Powodem tego jest to, że zero jest liczbą doskonale reprezentowalną w liczbach zmiennoprzecinkowych.

Mówiąc o doskonale reprezentowanych wartościach, otrzymujesz 24 bity zasięgu w ujęciu potęgi dwóch (pojedyncza precyzja). Zatem 1, 2, 4 są doskonale reprezentowalne, podobnie jak .5, .25 i .125. Dopóki wszystkie twoje ważne bity są 24-bitowe, jesteś złoty. Tak więc 10.625 można dokładnie przedstawić.

To świetnie, ale szybko rozpadnie się pod presją. Przychodzą mi na myśl dwa scenariusze: 1) Gdy w grę wchodzą obliczenia. Nie ufaj, że sqrt (3) * sqrt (3) == 3. Po prostu nie będzie tak. I prawdopodobnie nie będzie to epsilon, jak sugerują niektóre inne odpowiedzi. 2) Gdy w grę wchodzi jakikolwiek inny niż power-of-2 (NPOT). Może to zabrzmieć dziwnie, ale 0.1 jest nieskończoną serią binarną, a zatem wszelkie obliczenia obejmujące taką liczbę będą od początku nieprecyzyjne.

(Aha, a pierwotne pytanie wspomniało o porównaniach do zera. Nie zapominaj, że -0.0 jest również całkowicie poprawną wartością zmiennoprzecinkową.)


11

[Właściwa odpowiedź przeskakuje nad wyborem K. Wybieranie Kkończy się tak samo ad hoc jak wybieranie, VISIBLE_SHIFTale wybieranie Kjest mniej oczywiste, ponieważ w przeciwieństwie do VISIBLE_SHIFTtego nie jest oparte na żadnej właściwości wyświetlania. Wybierz swoją truciznę - wybierz Klub wybierz VISIBLE_SHIFT. Ta odpowiedź opowiada się za wyborem, VISIBLE_SHIFTa następnie pokazuje trudność w wyborze K]

Właśnie z powodu błędów okrągłych nie należy używać porównywania „dokładnych” wartości dla operacji logicznych. W konkretnym przypadku pozycji na wyświetlaczu wizualnym nie ma znaczenia, czy pozycja wynosi 0,0, czy 0,0000000003 - różnica jest niewidoczna dla oka. Więc twoja logika powinna wyglądać następująco:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Jednak ostatecznie „niewidoczny dla oka” będzie zależeć od właściwości ekranu. Jeśli możesz górną granicę wyświetlacza (powinieneś być w stanie); następnie wybierz VISIBLE_SHIFTułamek tej górnej granicy.

Teraz „poprawna odpowiedź” zależy od tego, Kwięc zbadajmy sposób wybierania K. „Właściwa odpowiedź” powyżej mówi:

K jest stałą, którą wybierzesz w taki sposób, że skumulowany błąd twoich obliczeń jest zdecydowanie ograniczony przez jednostki K w ostatnim miejscu (i jeśli nie jesteś pewien, czy poprawnie wyliczyłeś błąd związany z błędem, ustaw K kilka razy większy niż to, co obliczasz powiedzieć, że powinno być)

Więc potrzebujemy K. Jeśli uzyskanie Kjest trudniejsze, mniej intuicyjne niż wybranie mojego, VISIBLE_SHIFTwtedy zdecydujesz, co będzie dla ciebie odpowiednie. Aby to ustalić K, napiszemy program testowy, który analizuje wiele Kwartości, abyśmy mogli zobaczyć, jak się zachowuje. Powinno być oczywiste, jak wybrać K, jeśli można zastosować „właściwą odpowiedź”. Nie?

Wykorzystamy jako szczegóły „właściwej odpowiedzi”:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Spróbujmy wszystkich wartości K:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ach, więc K powinien wynosić 1e16 lub więcej, jeśli chcę, aby 1e-13 było „zero”.

Powiedziałbym, że masz dwie opcje:

  1. Wykonaj proste obliczenia epsilon, korzystając z osądu inżynieryjnego dla wartości „epsilon”, jak zasugerowałem. Jeśli tworzysz grafikę, a „zero” ma być „widoczną zmianą”, sprawdź swoje zasoby wizualne (obrazy itp.) I oceń, czym może być epsilon.
  2. Nie próbuj żadnych obliczeń zmiennoprzecinkowych, dopóki nie przeczytasz odniesienia do odpowiedzi nie-kultowej (i nie uzyskałeś doktoratu w tym procesie), a następnie nie wykorzystasz swojej intuicyjnej oceny, aby wybrać K.

10
Jednym z aspektów niezależności od rozdzielczości jest to, że nie można z całą pewnością stwierdzić, co oznacza „widoczna zmiana” w czasie kompilacji. To, co jest niewidoczne na ekranie super HD, może być bardzo oczywiste na ekranie o małym tyłku. Należy przynajmniej uczynić to funkcją wielkości ekranu. Lub nazwij to czymś innym.
Romain

1
Ale przynajmniej wybór „widocznego przesunięcia” opiera się na łatwo zrozumiałych właściwościach wyświetlania (lub ramki) - w przeciwieństwie do <poprawnej odpowiedzi>, Kktóra jest trudna i nie intuicyjna w wyborze.
GoZoner,

5

Prawidłowe pytanie: jak porównać punkty w Cocoa Touch?

Prawidłowa odpowiedź: CGPointEqualToPoint ().

Inne pytanie: czy dwie obliczone wartości są takie same?

Odpowiedź zamieszczona tutaj: nie są.

Jak sprawdzić, czy są blisko? Jeśli chcesz sprawdzić, czy są blisko, nie używaj CGPointEqualToPoint (). Ale nie sprawdzaj, czy są blisko. Rób coś, co ma sens w prawdziwym świecie, na przykład sprawdzając, czy punkt znajduje się poza linią lub czy znajduje się w kuli.


4

Ostatnim razem, gdy sprawdzałem standard C, nie było wymogu, aby operacje zmiennoprzecinkowe na liczbach podwójnych (w sumie 64 bity, 53-bitowa mantysa) były dokładne z większą dokładnością. Jednak niektóre urządzenia mogą wykonywać operacje w rejestrach z większą precyzją, a wymaganie to zostało zinterpretowane jako brak wymogu czyszczenia bitów niższego rzędu (poza dokładnością liczb ładowanych do rejestrów). Dzięki temu można uzyskać nieoczekiwane wyniki takich porównań w zależności od tego, co pozostało w rejestrach od tego, kto spał ostatni.

To powiedziawszy, i pomimo moich starań, aby go zniszczyć, gdy tylko go zobaczę, strój, w którym pracuję, ma dużo kodu C skompilowanego za pomocą gcc i działającego na Linuksie, i od bardzo dawna nie zauważyliśmy żadnego z tych nieoczekiwanych rezultatów. . Nie mam pojęcia, czy dzieje się tak, ponieważ gcc usuwa dla nas bity niskiego rzędu, 80-bitowe rejestry nie są używane do tych operacji na nowoczesnych komputerach, standard został zmieniony, czy co. Chciałbym wiedzieć, czy ktoś może zacytować rozdział i wiersz.


1

Możesz użyć takiego kodu do porównania liczby zmiennoprzecinkowej z zerem:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Porówna się to z dokładnością 0,1, co w tym przypadku wystarcza dla CGFloat.


Przesyłanie intbez ubezpieczenia theView.frame.origin.xznajduje się w / w pobliżu tego zakresu intprowadzi do nieokreślonego zachowania (UB) - lub w tym przypadku w 1/100 zakresu int.
chux - Przywróć Monikę

Nie ma absolutnie żadnego powodu, aby konwertować na taką liczbę całkowitą. Jak powiedział Chux, potencjał UB może pochodzić z wartości spoza zakresu; a na niektórych architekturach będzie to znacznie wolniejsze niż wykonywanie obliczeń zmiennoprzecinkowych. Wreszcie, pomnożenie przez 100 w ten sposób spowoduje porównanie z dokładnością do 0,01, a nie 0,1.
Sneftel,

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

Korzystam z następującej funkcji porównania, aby porównać liczbę miejsc po przecinku:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

Powiedziałbym, że właściwą rzeczą jest zadeklarowanie każdej liczby jako obiektu, a następnie zdefiniowanie w tym obiekcie trzech rzeczy: 1) operatora równości. 2) metoda setAcceptableDifference. 3) sama wartość. Operator równości zwraca wartość true, jeśli różnica bezwzględna dwóch wartości jest mniejsza niż wartość ustawiona jako akceptowalna.

Możesz podklasować obiekt, aby dopasować go do problemu. Na przykład okrągłe pręty metalowe o długości od 1 do 2 cali można uznać za jednakowej średnicy, jeśli ich średnice różnią się o mniej niż 0,0001 cala. Można więc wywołać setAcceptableDifference z parametrem 0.0001, a następnie z pewnością użyć operatora równości.


1
To nie jest dobra odpowiedź. Po pierwsze, cała „rzeczowa rzecz” nie robi nic, by rozwiązać problem. Po drugie, faktyczna implementacja „równości” nie jest w rzeczywistości poprawna.
Tom Swirly

3
Tom, może pomyślisz jeszcze raz o „rzeczach obiektowych”. Przy liczbach rzeczywistych reprezentowanych z dużą precyzją równość rzadko się zdarza. Ale czyjaś idea równości może być dostosowana, jeśli ci to odpowiada. Byłoby lepiej, gdyby istniał nadrzędny operator „w przybliżeniu równy”, ale tak nie jest.
John White,
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.