Чи безпечно перевіряти значення плаваючої точки на рівність 0?


100

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

Хоча я можу зрозуміти неточності між 0,00000000000001 та 0,00000000000002, 0 сам по собі здається досить важким зіпсувати, оскільки це просто нічого. Якщо ви ні до чого не чіткі, то це вже нічого.

Але я мало знаю про цю тему, тому мені це не сказати.

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

Чи завжди це повернеться правдою?


69
Потрійний оператор зайвий у цьому коді :)
— Joel Coehoorn

5
LOL ти маєш рацію. Ідіть мені
— Джин Робертс,

Я б цього не зробив, бо ти не знаєш, як х встановив нуль. Якщо ви все ще хочете це зробити, ви, мабуть, хочете закруглити або покласти х, щоб позбутися 1e-12 або такого, що може бути позначено в кінці.
— Рекс Логан

Відповіді:


115

Можна з упевненістю очікувати, що порівняння повернеться trueтоді і лише тоді, коли подвійна змінна має значення точно 0.0(що, звичайно, у вашому вихідному фрагменті коду - це, безумовно, так). Це відповідає семантиці ==оператора. a == bозначає " aдорівнює b".

Це небезпечно (бо це неправильно ) очікувати , що результат деякого розрахунку буде дорівнює нуль в подвійній (або в більш загальному плані , з плаваючою точкою) арифметики , коли результат такого ж розрахунок в чистій математиці дорівнює нуль. Це тому, що коли обчислення потрапляють у землю, з’являється помилка точності з плаваючою точкою - поняття, яке не існує в арифметиці реального числа математики.


51

Якщо вам потрібно зробити багато порівнянь "рівності", можливо, було б хорошою ідеєю написати трохи допоміжної функції або методу розширення в .NET 3.5 для порівняння:

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

Це можна використати наступним чином:

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

4
Ви можете мати помилку віднімання відміни, порівнюючи double1 і double2, якщо ці числа мають значення, дуже близькі одне до одного. Я б видалив Math.Abs ​​і перевірив кожну гілку окремо d1> = d2 - e і d1 <= d2 + e
— Теодор Зографос

"Оскільки Epsilon визначає мінімальне вираження позитивного значення, діапазон якого близько нуля, межа різниці між двома аналогічними значеннями повинен бути більшим, ніж Epsilon. Як правило, він у багато разів більший, ніж Epsilon. Через це ми рекомендуємо вам зробити не використовуйте Epsilon при порівнянні подвійних значень для рівності. " - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
— Рафаель Коста

15

Для вашого простого зразка цей тест добре. А як щодо цього:

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

Пам'ятайте, що .1 - це десятковий повтор у двійковій формі, і його неможливо точно представити. Потім порівняйте це з цим кодом:

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

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


5
Власне, це чомусь повертається істинним (принаймні, у LINQPad).
— Олексій Романов

Про що ".1 питання" ви говорите?
— Teejay

14

З запису MSDN для Double.Equals :

Точність у порівняннях

Метод рівних слід застосовувати обережно, оскільки два, мабуть, еквівалентні значення можуть бути неоднаковими через різну точність двох значень. Наступний приклад повідомляє, що значення подвійного .3333 та подвійне, повернене діленням 1 на 3, є неоднаковими.

...

Замість порівняння для рівності одна рекомендована методика передбачає визначення допустимої різниці між двома значеннями (наприклад, .01% одного із значень). Якщо абсолютна величина різниці між двома значеннями менша або дорівнює цій границі, різниця, ймовірно, буде обумовлена ​​різницею в точності, і, отже, значення, ймовірно, будуть рівні. Наступний приклад використовує цю методику для порівняння .33333 та 1/3, двох значень Double, які попередній приклад коду визнав нерівними.

Також див. Double.Epsilon .


1
Можливо також не зовсім еквівалентні значення порівняти як рівні. Можна було б сподіватись, що якщо x.Equals(y)тоді (1/x).Equals(1/y), але це не так, якщо xє 0і yє 1/Double.NegativeInfinity. Ці цінності оголошуються рівними, навіть якщо їх взаємна відповідь не відповідає.
— supercat

@supercat: Вони рівноцінні. І вони не мають взаємних доручень. Ви можете запустити тест знову x = 0і y = 0, і ви по- , як і раніше вважаєте , що 1/x != 1/y.
— Бен Войгт

@BenVoigt: З xі yяк тип double? Як ви порівнюєте результати, щоб вони звітували нерівними? Зауважимо, що 1 / 0,0 не є NaN.
— supercat

@supercat: Гаразд, це одна з речей, що IEEE-754 помиляється. (По-перше, це 1.0/0.0не може бути NaN таким, яким він має бути, оскільки межа не унікальна. По-друге, нескінченності порівнюються одна з одною, не звертаючи ніякої уваги на ступінь нескінченності)
— Бен Войгт

@BenVoigt: Якщо нуль був результатом множення двох дуже малих чисел, то ділення 1,0 на це повинно отримати значення, яке порівнює більше, ніж будь-яке число малих чисел, має однаковий знак, і менше, ніж будь-яке число, якщо одне з малих числа мали протилежні знаки. ІМХО, IEEE-754 було б краще, якби він мав непідписаний нуль, але позитивні та негативні нескінченні величини.
— supercat

6

Проблема виникає, коли ви порівнюєте різні типи реалізації значень з плаваючою комою, наприклад, порівняння поплавця з подвійним. Але з тим самим типом це не повинно бути проблемою.

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

Проблема полягає в тому, що програміст іноді забуває, що для порівняння відбувається неявний тип передачі (подвійний плавати), і це призводить до помилок.


3

Якщо число було прямо присвоєне поплавцю або подвійному, то можна перевірити проти нуля або будь-якого цілого числа, яке може бути представлене в 53 бітах для подвійного або 24 біта для поплавця.

Або кажучи іншим способом, ви завжди можете присвоїти ціле ціле числу подвійному, а потім порівняти подвійне назад з тим же цілим числом і гарантовано воно буде рівним.

Ви також можете почати, призначивши ціле число, і прості порівняння продовжують працювати, дотримуючись додавання, віднімання чи множення на цілі числа (якщо припустити, що результат становить менше 24 біт для поплавця і 53 53 біт на дубль). Таким чином, ви можете ставитися до поплавків і парних чисел як цілих чисел за певних контрольованих умов.


Я погоджуюся з вашим твердженням загалом (і його схвалив), але я вважаю, що це дійсно залежить від того, застосовується чи ні реалізація IEEE 754 з плаваючою точкою. І я вважаю, що кожен "сучасний" комп'ютер використовує IEEE 754, принаймні для зберігання поплавців (є дивні правила округлення, які відрізняються).
— Марк Лаката

2

Ні, це не нормально. Так звані денормалізовані значення (субнормальні) при порівнянні дорівнюють 0,0, порівнюються як помилкові (ненульові), але при використанні в рівнянні нормалізуються (стають 0,0). Таким чином, використовувати це як механізм уникнення поділу на нуль не є безпечним. Натомість додайте 1,0 та порівняйте з 1,0. Це забезпечить, що всі субнормальні тварини трактуються як нульові.


Субнормальні тварини також відомі як деннормали
— Мануель

При використанні субнормалів не дорівнюють нулю, хоча залежно від точної операції вони можуть мати або не давати однакового результату.
— похмуріть

-2

Спробуйте це, і ви побачите, що == не є надійним для double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Ось відповідь від Quora.


-4

Насправді, я думаю, що краще використовувати наступні коди для порівняння подвійного значення проти 0,0:

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

Те саме для float:

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

5
Ні. Від документів з double.Epsilon: "Якщо ви створюєте користувацький алгоритм, який визначає, чи можна вважати два числа з плаваючою комою рівними, ви повинні використовувати значення, яке перевищує константу Epsilon, щоб встановити прийнятний абсолютний запас різниці щоб два значення вважалися рівними. (Як правило, різниця в різниці в багато разів більша, ніж Епсілон.) "
— Alastair Maw

1
@AlastairMaw це стосується перевірки рівності двох дублів будь-якого розміру. Для перевірки рівності до нуля подвійно. Епсилон добре.
— jwg

4
Ні, це не так . Цілком імовірно, що значення, до якого ви потрапили за допомогою деякого обчислення, багато разів епсилон від нуля, але все ж слід вважати нулем. Ви не досягнете чарівного куба додаткової точності у своєму проміжному результаті звідкись, лише тому, що це відбувається майже до нуля.
— Alastair Maw

4
Наприклад: (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 (і значно так у величині: 2,78E-17 проти 4,94E -324)
— Аластер Мау

Отже, яка точність рекомендується, якщо подвійна. Епсілон не в порядку? Буде 10 разів епсілон добре? 100 разів?
— laang
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.