Porównaj podwójnie do zera za pomocą epsilon

214

Dzisiaj przeglądałem kod C ++ (napisany przez kogoś innego) i znalazłem tę sekcję:

double someValue = ...
if (someValue <  std::numeric_limits<double>::epsilon() && 
    someValue > -std::numeric_limits<double>::epsilon()) {
  someValue = 0.0;
}

Próbuję dowiedzieć się, czy to w ogóle ma sens.

Dokumentacja epsilon()mówi:

Funkcja zwraca różnicę między 1 a najmniejszą wartością większą niż 1, która jest reprezentowalna [podwójnie].

Czy dotyczy to również 0, tj epsilon(). Czy najmniejsza wartość jest większa od 0? Czy też są liczby pomiędzy 0i 0 + epsilonktóre mogą być reprezentowane przez double?

Jeśli nie, to czy porównanie nie jest równoważne someValue == 0.0?

Sebastian Krysmański
źródło
3
Wartość epsilon wokół 1 najprawdopodobniej będzie znacznie wyższa niż około 0, więc prawdopodobnie będą wartości od 0 do 0 + epsilon_at_1. Wydaje mi się, że autor tej sekcji chciał użyć czegoś małego, ale nie chciał użyć magicznej stałej, więc użył tej zasadniczo arbitralnej wartości.
enobayram
2
Porównywanie liczb zmiennoprzecinkowych jest trudne, a użycie epsilonu lub wartości progowej jest nawet zalecane. Proszę odnieść się do: cs.princeton.edu/introcs/91float and cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Aditya Kumar Pandey
40
Pierwszy link to 403.99999999
graham.reeds
6
IMO, w tym przypadku użycie numeric_limits<>::epsilonjest mylące i nie ma znaczenia. Chcemy założyć 0, jeśli rzeczywista wartość różni się nie więcej niż o jakieś ε od ​​0. I ε powinno być wybrane na podstawie specyfikacji problemu, a nie na wartości zależnej od maszyny. Podejrzewam, że obecny epsilon jest bezużyteczny, ponieważ nawet kilka operacji FP może skumulować większy błąd.
Andrey Vihrov,
1
+1. epsilon nie jest najmniejszym możliwym, ale może służyć do określonego celu w większości praktycznych zadań inżynierskich, jeśli wiesz, jakiej precyzji potrzebujesz i co robisz.
SChepurin,

Odpowiedzi:

192

Zakładając, że 64-bitowy podwójny IEEE ma 52-bitową mantysę i 11-bitowy wykładnik. Podzielmy to na kawałki:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1

Najmniejsza reprezentowalna liczba większa niż 1:

1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52

W związku z tym:

epsilon = (1 + 2^-52) - 1 = 2^-52

Czy są jakieś liczby od 0 do epsilon? Dużo ... Np. Minimalna liczba reprezentatywna (normalna) to:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022

W rzeczywistości istnieją (1022 - 52 + 1)×2^52 = 4372995238176751616liczby od 0 do epsilon, co stanowi 47% wszystkich dodatnich liczb reprezentowalnych ...

Jakow Gałka
źródło
27
Tak dziwne, że można powiedzieć „47% liczb dodatnich” :)
konfigurator
13
@configurator: Nie, nie można tego powiedzieć (nie ma „naturalnej” skończonej miary). Ale możesz powiedzieć „47% dodatnich liczb reprezentatywnych ”.
Jakow Galka,
1
@ybungalobill Nie mogę tego rozgryźć. Wykładnik ma 11 bitów: 1 bit znaku i 10 bitów wartości. Dlaczego 2 ^ -1022, a nie 2 ^ -1024 jest najmniejszą liczbą dodatnią?
Pavlo Dyban,
3
@PavloDyban: po prostu dlatego, że wykładniki nie mają bitu znakowego . Są one kodowane jako przesunięcia: jeśli zakodowanym wykładnikiem jest 0 <= e < 2048mantysa, pomnożona jest przez 2 do potęgi e - 1023. Np. Wykładnik z 2^0jest zakodowany jako e=1023, 2^1jak e=1024i 2^-1022jako e=1. Wartość e=0jest zarezerwowana dla podnormalnych i rzeczywistego zera.
Jakow Galka,
2
@PavloDyban: również 2^-1022jest najmniejszą liczbą normalną . Najmniejsza liczba to tak naprawdę 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Jest to nienormalne, co oznacza, że ​​część mantysy jest mniejsza niż 1, więc jest zakodowana wykładnikiem e=0.
Yakov Galka
17

Test na pewno nie jest taki sam jak someValue == 0. Cała idea liczb zmiennoprzecinkowych polega na tym, że przechowują one wykładnik i znaczenie. Reprezentują zatem wartość z pewną liczbą binarnych znaczących liczb precyzji (53 w przypadku podwójnego IEEE). Reprezentatywne wartości są znacznie gęstiej upakowane w pobliżu 0 niż w pobliżu 1.

Aby użyć bardziej znanego systemu dziesiętnego, załóżmy, że przechowujesz wartość dziesiętną „do 4 cyfr znaczących” z wykładnikiem wykładniczym. Następnie następna reprezentowalna wartość większa niż 1jest 1.001 * 10^0i epsilonjest 1.000 * 10^-3. Ale 1.000 * 10^-4jest również reprezentowalny, zakładając, że wykładnik może przechowywać -4. Możesz wierzyć mi na słowo, że podwójne IEEE może przechowywać wykładniki mniejsze niż wykładnik epsilon.

Nie można stwierdzić na podstawie samego kodu, czy ma sens, czy nie należy go używać epsilonjako granicy, należy spojrzeć na kontekst. Może to być epsilonuzasadnione oszacowanie błędu w obliczeniach, które się pojawiło someValue, i może być tak, że nie jest.

Steve Jessop
źródło
2
Dobra uwaga, ale nawet jeśli tak jest, lepszym rozwiązaniem byłoby ograniczenie błędu w rozsądnie nazwanej zmiennej i użycie go do porównania. W obecnej postaci nie różni się niczym od stałej magicznej.
enobayram,
Być może powinienem był wyjaśnić moje pytanie: nie kwestionowałem, czy epsilon był wystarczająco dużym „progiem” na pokrycie błędu obliczeniowego, ale czy to porównanie jest równe, someValue == 0.0czy nie.
Sebastian Krysmanski,
13

Istnieją liczby, które istnieją między 0 a epsilon, ponieważ epsilon jest różnicą między 1 a następną najwyższą liczbą, którą można przedstawić powyżej 1, a nie różnicą między 0 a następną najwyższą liczbą, którą można przedstawić powyżej 0 (jeśli tak, to że kod zrobiłby bardzo mało): -

#include <limits>

int main ()
{
  struct Doubles
  {
      double one;
      double epsilon;
      double half_epsilon;
  } values;

  values.one = 1.0;
  values.epsilon = std::numeric_limits<double>::epsilon();
  values.half_epsilon = values.epsilon / 2.0;
}

Za pomocą debugera zatrzymaj program na końcu main i spójrz na wyniki, a zobaczysz, że epsilon / 2 różni się od epsilon, zero i jeden.

Ta funkcja przyjmuje więc wartości pomiędzy +/- epsilon i czyni je zerami.

Skizz
źródło
5

Przybliżenie epsilon (najmniejsza możliwa różnica) wokół liczby (1,0, 0,0, ...) można wydrukować za pomocą następującego programu. Wyświetla następujące dane wyjściowe:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Trochę myślenia wyjaśnia, że ​​epsilon staje się mniejszy, im mniejszą liczbę używamy do sprawdzania jej wartości epsilon, ponieważ wykładnik może dostosować się do wielkości tej liczby.

#include <stdio.h>
#include <assert.h>
double getEps (double m) {
  double approx=1.0;
  double lastApprox=0.0;
  while (m+approx!=m) {
    lastApprox=approx;
    approx/=2.0;
  }
  assert (lastApprox!=0);
  return lastApprox;
}
int main () {
  printf ("epsilon for 0.0 is %e\n", getEps (0.0));
  printf ("epsilon for 1.0 is %e\n", getEps (1.0));
  return 0;
}
pbhd
źródło
2
Jakie wdrożenia sprawdziłeś? Z pewnością nie jest tak w przypadku GCC 4.7.
Anton Golov,
3

Załóżmy, że pracujemy z zabawkowymi liczbami zmiennoprzecinkowymi, które pasują do 16-bitowego rejestru. Jest bit znaku, 5-bitowy wykładnik i 10-bitowa mantysa.

Wartością tej liczby zmiennoprzecinkowej jest mantysa, interpretowana jako binarna wartość dziesiętna, razy dwa do potęgi wykładnika.

Około 1 wykładnik równa się zero. Najmniejsza cyfra mantysy to jedna część na 1024.

Blisko 1/2 wykładnika wynosi minus jeden, więc najmniejsza część mantysy jest o połowę mniejsza. Z pięciobitowym wykładnikiem może osiągnąć wartość ujemną 16, w którym to momencie najmniejsza część mantysy jest warta jedną część na 32m. Przy ujemnym wykładniku 16 wartość wynosi około jednej części na 32k, znacznie bliżej zera niż epsilon wokół tego, który obliczyliśmy powyżej!

Teraz jest to zabawkowy model zmiennoprzecinkowy, który nie odzwierciedla wszystkich dziwactw prawdziwego systemu zmiennoprzecinkowego, ale zdolność do odzwierciedlania wartości mniejszych niż epsilon jest dość podobna do rzeczywistych wartości zmiennoprzecinkowych.

Jak - Adam Nevraumont
źródło
3

Różnica między Xkolejną wartością a kolejną wartością Xróżni się w zależności od X.
epsilon()jest tylko różnicą między 1i następną wartością 1.
Różnica pomiędzy 0i następną wartością 0nie jest epsilon().

Zamiast tego możesz użyć std::nextafterdo porównania podwójnej wartości z 0następującymi:

bool same(double a, double b)
{
  return std::nextafter(a, std::numeric_limits<double>::lowest()) <= b
    && std::nextafter(a, std::numeric_limits<double>::max()) >= b;
}

double someValue = ...
if (same (someValue, 0.0)) {
  someValue = 0.0;
}
Daniel Laügt
źródło
2

Myślę, że to zależy od precyzji twojego komputera. Spójrz na tę tabelę : możesz zobaczyć, że jeśli twój epsilon jest reprezentowany przez dwukrotność, ale twoja precyzja jest wyższa, porównanie nie jest równoważne z

someValue == 0.0

W każdym razie dobre pytanie!

Luca Davanzo
źródło
2

Nie możesz zastosować tego do 0, z powodu części mantysy i wykładnika. Z powodu wykładnika możesz przechowywać bardzo małe liczby, które są mniejsze niż epsilon, ale gdy spróbujesz zrobić coś takiego (1.0 - „bardzo mała liczba”), otrzymasz 1.0. Epsilon jest wskaźnikiem nie wartości, ale precyzji wartości, która znajduje się w mantysie. Pokazuje, ile poprawnych kolejnych cyfr dziesiętnych liczby możemy zapisać.

Arsenii Fomin
źródło
2

Z zmiennoprzecinkową wartością IEEE, pomiędzy najmniejszą niezerową wartością dodatnią i najmniejszą niezerową wartością ujemną, istnieją dwie wartości: zero dodatnie i zero ujemne. Testowanie, czy wartość mieści się między najmniejszymi niezerowymi wartościami, jest równoważne testowaniu na równość z zerem; przypisanie może jednak mieć wpływ, ponieważ zmieniłoby ujemne zero na dodatnie zero.

Można sobie wyobrazić, że format zmiennoprzecinkowy może mieć trzy wartości między najmniejszymi skończonymi wartościami dodatnimi i ujemnymi: dodatni nieskończenie mały, zero bez znaku i ujemny nieskończenie mały. Nie znam żadnych formatów zmiennoprzecinkowych, które w rzeczywistości działałyby w ten sposób, ale takie zachowanie byłoby całkowicie rozsądne i prawdopodobnie lepsze niż IEEE (być może nie na tyle lepsze, aby warto było dodać dodatkowy sprzęt do obsługi, ale matematycznie 1 / (1 / INF), 1 / (- 1 / INF) i 1 / (1-1) powinny reprezentować trzy różne przypadki ilustrujące trzy różne zera). Nie wiem, czy jakikolwiek standard C nakazałby, aby podpisane nieskończenie małe, jeśli istnieją, musiałyby się równać zero. Jeśli nie, kod taki jak powyższy może pożytecznie zapewnić, że np

supercat
źródło
Czy „1 / (1-1)” (z twojego przykładu) jest nieskończonością, a nie zero?
Sebastian Krysmanski,
Ilości (1-1), (1 / INF) i (-1 / INF) wszystkie oznaczają zero, ale podzielenie liczby dodatniej przez każdą z nich powinno teoretycznie dać trzy różne wyniki (matematyka IEEE uważa pierwsze dwa za identyczne ).
supercat,
1

Powiedzmy, że system nie może odróżnić 1.000000000000000000000 i 1.000000000000000000001. to jest 1,0 i 1,0 + 1e-20. Czy uważasz, że nadal istnieją pewne wartości, które można przedstawić między -1e-20 a + 1e-20?

cababunga
źródło
Z wyjątkiem zera, nie sądzę, że istnieją wartości od -1e-20 do + 1e-20. Ale tylko dlatego, że myślę, że to nie prawda.
Sebastian Krysmanski,
@SebastianKrysmanski: to nieprawda, istnieje wiele wartości zmiennoprzecinkowych między 0 a epsilon. Ponieważ jest to zmiennoprzecinkowy , a nie stały punkt.
Steve Jessop,
Najmniejsza reprezentowalna wartość, która jest różna od zera, jest ograniczona liczbą bitów przydzielonych do reprezentowania wykładnika wykładniczego. Więc jeśli double ma wykładnik 11-bitowy, najmniejsza liczba to 1e-1023.
cababunga
0

Ponadto dobrym powodem takiej funkcji jest usunięcie „denormałów” (te bardzo małe liczby, które nie mogą już używać domyślnego wiodącego „1” i mają specjalną reprezentację FP). Dlaczego chcesz to zrobić? Ponieważ niektóre maszyny (w szczególności niektóre starsze Pentium 4) działają naprawdę bardzo wolno podczas przetwarzania denormałów. Inne stają się nieco wolniejsze. Jeśli twoja aplikacja tak naprawdę nie potrzebuje tych bardzo małych liczb, dobrym rozwiązaniem jest spłukanie ich do zera. Dobrym miejscem do rozważenia tego są ostatnie kroki filtrów IIR lub funkcji rozpadu.

Zobacz także: Dlaczego zmiana 0,1f na 0 spowalnia działanie 10-krotnie?

i http://en.wikipedia.org/wiki/Denormal_number

Dithermaster
źródło
1
Usuwa to znacznie więcej liczb niż tylko numery zdenormalizowane. Zmienia stałą Plancka lub masę elektronu na zero, co da bardzo, bardzo błędne wyniki, jeśli użyjesz tych liczb.
gnasher729