Чи безпечно перевіряти значення плаваючої точки на рівність 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.