Порівняйте подвійний до нуля за допомогою epsilon


214

Сьогодні я переглянув якийсь код C ++ (написав хтось інший) і знайшов цей розділ:

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

Я намагаюся розібратися, чи це навіть має сенс.

Документація для epsilon():

Функція повертає різницю між 1 і найменшим значенням, більшим за 1, яке можна представити [подвійним].

Чи застосовується це і до 0, тобто epsilon()найменше значення перевищує 0? Або є числа між 0і 0 + epsilonякі можуть бути представлені double?

Якщо ні, то чи не порівняння еквівалентно someValue == 0.0?


3
Епсилон навколо 1, швидше за все, буде набагато вище, ніж близько 0, тому, ймовірно, будуть значення між 0 і 0 + epsilon_at_1. Я думаю, автор цього розділу хотів використати щось невелике, але він не хотів використовувати магічну константу, тому він просто використав це, по суті, довільне значення.
енобайрам

2
Порівнювати номери з плаваючою комою важко, і навіть рекомендується використовувати епсилон або порогове значення. Будь ласка, зверніться до: cs.princeton.edu/introcs/91float and cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Aditya Kumar Pandey

40
Перше посилання - 403.99999999
graham.reeds

6
ІМО, у цьому випадку використання numeric_limits<>::epsilonвводить в оману та не має значення. Ми хочемо припустити 0, якщо фактичне значення відрізняється не більш ніж на деякий ε від 0. І ε слід вибирати виходячи із специфікації задачі, а не від машинно залежного значення. Я б підозрював, що поточний епсілон марний, оскільки навіть лише кілька операцій ПП можуть накопичити помилку, більшу від цього.
Андрій Віхров

1
+1. epsilon не є найменшим можливим, але може служити заданій меті в більшості практичних інженерних завдань, якщо ви знаєте, яка точність вам потрібна і що ви робите.
SChepurin

Відповіді:


192

Припускаючи, що 64-розрядний IEEE подвійний, є 52-бітна мантіса та 11-бітний показник. Розбимо його на біти:

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

Найменше представницьке число більше 1:

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

Тому:

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

Чи є цифри між 0 і epsilon? Чимало ... Наприклад, мінімальне додатне (нормальне) число:

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

Насправді є (1022 - 52 + 1)×2^52 = 4372995238176751616цифри між 0 і epsilon, що становить 47% від усіх позитивних репрезентативних чисел ...


27
Так дивно, що ви можете сказати "47% від позитивних чисел" :)
конфігуратор

13
@configurator: Ні, ви цього не можете сказати (ніякої "природної" кінцевої міри не існує). Але можна сказати "47% позитивних представницьких чисел".
Яків Галка

1
@ybungalobill Я не можу це зрозуміти. Експонент має 11 біт: 1 біт знака і 10 біт значення. Чому 2 ^ -1022, а не 2 ^ -1024 - найменше додатне число?
Павло Дибань

3
@PavloDyban: просто тому, що експоненти не мають бітових знаків. Вони кодуються як компенсації: якщо закодований показник - 0 <= e < 2048мантісса множиться на 2 на потужність e - 1023. Наприклад, показник 2^0кодується як e=1023, 2^1як e=1024і 2^-1022як e=1. Значення e=0зарезервовано для субнормалів і реальний нуль.
Яків Галка

2
@PavloDyban: також 2^-1022найменше нормальне число. Найменша кількість насправді 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Це субнормально, тобто частина мантіси менша за 1, тому вона кодується експонентом e=0.
Яків Галка

17

Тест, безумовно, не такий, як someValue == 0. Вся ідея чисел з плаваючою комою полягає в тому, що вони зберігають показник і значення. Тому вони представляють значення з певною кількістю двійкових значущих цифр точності (53 у випадку подвійного IEEE). Значення, що відображаються, набагато більш щільно упаковані біля 0, ніж близько 1.

Для використання більш звичної десяткової системи, припустимо, ви зберігаєте десяткове значення "до 4 значущих цифр" з експонентом. Тоді наступне репрезентативне значення більше, ніж 1є 1.001 * 10^0, і epsilonє 1.000 * 10^-3. Але 1.000 * 10^-4також є репрезентативним, припускаючи, що експонент може зберігати -4. Ви можете прийняти моє слово, що подвійний IEEE може зберігати експоненти менше, ніж показник epsilon.

З цього коду ви не можете самостійно сказати, чи є сенс або не використовувати epsilonконкретно в якості зв’язаного, вам потрібно подивитися на контекст. Можливо, epsilonце є розумною оцінкою помилки в отриманому розрахунку someValue, а може бути, що це не так.


2
Хороший момент, але навіть якщо це так, кращою практикою було б зберігати помилку, пов'язану в розумно названій змінній, і використовувати її для порівняння. На сьогодні він нічим не відрізняється від магічної константи.
енобайрам

Можливо, я мав би бути яснішим у своєму питанні: я не питав, чи є epsilon достатньо великим «порогом» для покриття обчислювальної помилки, але чи це порівняння рівне someValue == 0.0чи ні.
Себастьян Крисманскі

13

Існують числа, які існують між 0 і epsilon, оскільки epsilon - це різниця між 1 і наступним найбільшим числом, яке може бути представлено вище 1, а не різниця між 0 і наступним найбільшим числом, яке може бути представлено вище 0 (якби воно було, що код зробив би дуже мало): -

#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;
}

За допомогою налагоджувача зупиніть програму в кінці основного і подивіться на результати, і ви побачите, що epsilon / 2 відрізняється від epsilon, zero та one.

Таким чином, ця функція приймає значення між +/- epsilon і робить їх нульовими.


5

Приблизна кількість епсилона (найменша можлива різниця) навколо числа (1,0, 0,0, ...) може бути надрукована за допомогою наступної програми. Він друкує наступний результат:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Трохи роздумуючи, стає зрозумілим, що епсилон стає меншим, чим меншим є число, яке ми використовуємо для перегляду його epsilon-значення, оскільки показник може підлаштовуватися під розмір цього числа.

#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;
}

2
Які реалізації ви перевірили? Це точно не стосується GCC 4.7.
Антон Голов

3

Припустимо, ми працюємо з іграшковими номерами з плаваючою комою, що входять у 16-бітний регістр. Є бітовий знак, 5-бітний показник і 10-бітна мантіса.

Значенням цього числа з плаваючою комою є мантіса, інтерпретована як двійкове десяткове значення, два рази до потужності показника.

Приблизно 1 показник дорівнює нулю. Отже найменша цифра мантіси - одна частина 1024 року.

Близько 1/2 показника - мінус один, тому найменша частина мантіси наполовину більша. Якщо п'ятибітний показник може досягати від’ємного 16, в цей момент найменша частина мантіси коштує однієї частини в 32м. А при від'ємному 16 експоненті значення становить приблизно одну частину в 32k, набагато ближче до нуля, ніж епсилон навколо однієї, яку ми обчислили вище!

Тепер це модель з плаваючою крапкою з іграшкою, яка не відображає всіх химерностей реальної системи з плаваючою точкою, але здатність відображати значення, менші за епсилон, досить подібні до реальних значень з плаваючою комою.


3

Різниця між Xта наступними значеннями Xзмінюється залежно від X.
epsilon()є лише різниця між 1наступним значенням і наступним значенням 1.
Різниця між 0і наступним значенням 0не є epsilon().

Натомість ви можете використовувати std::nextafterдля порівняння подвійного значення з 0наступним:

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;
}

2

Я думаю, що це залежить від точності вашого комп’ютера. Погляньте на цю таблицю : ви можете бачити, що якщо ваш епсилон представлений подвійною, але ваша точність вище, порівняння не еквівалентно

someValue == 0.0

Гарне питання все одно!


2

Ви не можете застосувати це до 0 через мантіси та складових частин. Завдяки експоненту ви можете зберігати дуже мало цифр, які менші, ніж епсілон, але якщо ви спробуєте зробити щось на кшталт (1,0 - «дуже невелике число»), ви отримаєте 1,0. Епсілон - це показник не цінності, а ціннісної точності, яка є в мантісі. Він показує, скільки правильних послідовних десяткових цифр числа ми можемо зберігати.


2

З плаваючою точкою IEEE, між найменшим ненульовим позитивним значенням і найменшим ненульовим негативним значенням, існує два значення: позитивний нуль і від’ємний нуль. Тестування того, чи є значення між найменшими ненульовими значеннями, еквівалентно тестуванню на рівність нулю; присвоєння, однак, може мати ефект, оскільки воно змінить негативний нуль на позитивний нуль.

Можна вважати, що формат з плаваючою комою може мати три значення між найменшими кінцевими позитивними та негативними значеннями: позитивне нескінченне мале, беззначне нульове значення та негативне нескінченне мінімальне. Я не знайомий з будь-якими форматами з плаваючою комою, які насправді так працюють, але така поведінка була б цілком розумною і, мабуть, кращою, ніж IEEE (можливо, недостатньо краще, щоб варто було додати додаткове обладнання для його підтримки, але математично 1 / (1 / INF), 1 / (- 1 / INF) та 1 / (1-1) повинні являти собою три різних випадки, що ілюструють три різні нулі). Я не знаю, чи мав би бути який-небудь стандарт С, який підписав нескінченно великі особи, якщо вони існують, мав би порівнювати рівний нулю. Якщо цього не зробити, такий код, як описано вище, може бути корисним, щоб, наприклад,


Чи не "1 / (1-1)" (з вашого прикладу) нескінченність, а не нуль?
Себастьян Крисманскі

Величини (1-1), (1 / INF) і (-1 / INF) усі являють собою нуль, але поділ додатного числа на кожне з них має теоретично дати три різні результати (математика IEEE вважає перші два як однакові ).
supercat

1

Отже, скажімо, система не може розрізняти 1.000000000000000000000 та 1.000000000000000000001. тобто 1,0 і 1,0 + 1e-20. Як ви вважаєте, все ще є деякі значення, які можна представити між -1e-20 та + 1e-20?


За винятком нуля, я не думаю, що є значення від -1e-20 до + 1e-20. Але тільки тому, що я думаю, це не робить це правдою.
Себастьян Крисманскі

@SebastianKrysmanski: це неправда, є багато значень з плаваючою комою між 0 і epsilon. Тому що це плаваюча точка, а не фіксована точка.
Стів Джессоп

Найменше представлене значення, яке відрізняється від нуля, обмежено кількістю бітів, виділених для представлення експонента. Тож якщо у подвійного має 11 бітний показник, найменше число буде 1e-1023.
кабабунга

0

Також вагома причина виникнення такої функції є видалення "деннормалів" (тих дуже малих чисел, які вже не можуть використовувати маючи на увазі провідну "1" і мають спеціальне представлення ПП). Чому б ти хотів це зробити? Оскільки деякі машини (зокрема, деякі старіші Pentium 4s) отримують дуже-дуже повільно під час обробки денромалій. Інші стають дещо повільніше. Якщо вашій програмі насправді не потрібні ці дуже малі цифри, зведення їх до нуля є хорошим рішенням. Хороші місця для розгляду цього питання - це останні кроки будь-яких фільтрів IIR або функцій розпаду.

Дивіться також: Чому зміна 0,1f на 0 уповільнює продуктивність на 10 разів?

та http://en.wikipedia.org/wiki/Denormal_number


1
Це видаляє набагато більше чисел, ніж просто денормалізовані числа. Це змінює константу Планка або масу електрона до нуля, що дасть вам дуже, дуже неправильні результати, якщо ви будете використовувати ці числа.
gnasher729
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.