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


391

Я знаю, UIKitвикористовує CGFloatчерез незалежну систему координат роздільної здатності.

Але кожен раз, коли я хочу перевірити, чи frame.origin.xє, наприклад , 0це мені нудно:

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

Чи не CGFloatвразлива до помилкових спрацьовувань при порівнянні з ==, <=, >=, <, >? Це плаваюча точка, і вони мають проблеми з неточністю: 0.0000000000041наприклад.

Чи Objective-Cповодиться з цим внутрішньо при порівнянні чи може статися так, що а, origin.xщо читається як нуль, не порівнюється з 0істинним?

Відповіді:


466

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

Перш за все, якщо ви хочете програмувати з плаваючою точкою, слід прочитати це:

Що повинен знати кожен вчений-комп’ютер про арифметику з плаваючою комою

Так, прочитайте все це. Якщо це занадто велике навантаження, слід використовувати цілі числа / фіксовану точку для своїх розрахунків, поки не встигнете її прочитати. :-)

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

  1. Той факт, що багато значень, які ви можете записати у джерело, чи прочитати з scanfабо strtod, не існують як значення з плаваючою комою та мовчки перетворюються на найближче наближення. Про це йшлося у відповіді demon9733.

  2. Той факт, що багато результатів округляються через недостатню точність, щоб представити фактичний результат. Простий приклад, коли ви можете бачити це - додавання x = 0x1fffffeта y = 1плавання. Тут xє 24 біта точності в мантісі (гаразд) і yмає всього 1 біт, але коли ви додаєте їх, їх біти знаходяться не в місцях, що перекриваються, і в результаті знадобиться 25 біт точності. Натомість він стає округленим (до 0x2000000режиму округлення за замовчуванням).

  3. Той факт, що багато результатів округлюються через необхідність нескінченно багато місць для правильного значення. Це включає в себе як раціональні результати, такі як 1/3 (які ви знайомі з десяткової, де вона займає нескінченно багато місць), а також 1/10 (що також займає нескінченно багато місць у двійковій формі, оскільки 5 не є силою 2), а також нераціональні результати, такі як квадратний корінь усього, що не є ідеальним квадратом.

  4. Подвійне округлення. У деяких системах (зокрема x86) вирази з плаваючою комою оцінюються з більшою точністю, ніж їх номінальні типи. Це означає, що коли трапиться один із перерахованих вище типів округлення, ви отримаєте два етапи округлення, спочатку округлення результату до типу більш точної, потім округлення до кінцевого типу. Як приклад, розглянемо, що відбувається у десятковій формі, якщо округлити 1,49 до цілого числа (1), а порівняно з тим, що спочатку округлите його до одного десяткового знаку (1,5), а потім округлите цей результат до цілого числа (2). Це насправді одна з найнебезпечніших областей, з якою потрібно боротися в плаваючій точці, оскільки поведінка компілятора (особливо для помилкових, невідповідних компіляторів, як GCC) непередбачувана.

  5. Трансцендентні функції ( trig, exp, logі т.д.) не визначені , щоб правильно округлені результати; результат вказується просто правильним у межах однієї одиниці в останньому місці точності (зазвичай це називається 1ulp ).

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

Редагувати: Зокрема, перевірка епсилону відносної величини повинна виглядати приблизно так:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

Звідки FLT_EPSILONконстанта з float.h(замініть її DBL_EPSILONна doubles або LDBL_EPSILONдля long doubles) і Kє константою, ви виберете таке, що накопичена помилка ваших обчислень напевно обмежена Kодиницями на останньому місці (і якщо ви не впевнені, що ви отримали помилку правильний розрахунок, зробіть Kв кілька разів більше, ніж те, що ваші розрахунки говорять, що має бути).

Нарешті, зауважте, що якщо ви користуєтесь цим, може знадобитися якийсь особливий догляд поблизу нуля, оскільки FLT_EPSILONне має сенсу для денноматок. Швидке виправлення було б зробити:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

і аналогічно замінити, DBL_MINякщо використовувати парні.


25
fabs(x+y)проблематично, якщо xі y(може) мати різні ознаки. І все-таки хороша відповідь проти припливу вантажно-культових порівнянь.
Даніель Фішер

27
Якщо xі yмають різні ознаки, це не проблема. Права сторона буде "занадто маленькою", але оскільки xі yмають інший знак, вони все одно не повинні порівнювати рівних. (Якщо вони не настільки малі, щоб бути denormal, але тоді другий ловить випадку це)
R .. GitHub СТОП допоміг ICE

4
Мені цікаво ваше твердження: "особливо для баггі, невідповідних компіляторів, таких як GCC". Чи дійсно GCC-баггі, а також невідповідний?
Nicolás Ozimica

3
Оскільки питання позначено iOS, варто відзначити, що компілятори Apple (як clang, так і gcc-версії Apple) завжди використовували FLT_EVAL_METHOD = 0 і намагаються бути абсолютно суворими щодо того, щоб не мати зайвої точності. Якщо ви виявите будь-які порушення щодо цього, будь ласка, подайте звіти про помилки.
Стівен Канон

17
"Перш за все, значення з плаваючою комою не є" випадковими "у своїй поведінці. Точне порівняння може і має сенс у багатьох реальних звичаях". - Всього два речення і вже заробив +1! Це одне з найбільш тривожних помилок, які люди роблять при роботі з плаваючими точками.
Крістіан Рау

36

Оскільки 0 точно представлений як номер IEEE754 з плаваючою комою (або з використанням будь-якої іншої реалізації fp чисел, з якими я коли-небудь працював) порівняння з 0, ймовірно, безпечно. Однак ви можете покусати, якщо ваша програма обчислить значення (наприклад theView.frame.origin.x), яке, як ви вважаєте, має бути 0, але яке не може гарантувати 0.

Щоб трохи уточнити, такі обчислення:

areal = 0.0

буде (якщо ваша мова чи система порушена) створить таке значення, яке (areal == 0,0) повертає істину, але інше обчислення, наприклад

areal = 1.386 - 2.1*(0.66)

не можна.

Якщо ви можете запевнити себе, що ваші обчислення дають значення, що дорівнюють 0 (а не лише те, що вони дають значення, які повинні бути 0), тоді ви можете продовжувати і порівнювати значення fp з 0. Якщо ви не можете переконати себе в необхідній мірі , найкраще дотримуватися звичайного підходу до "толерантності рівності".

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

Для Angry Birds не так небезпечно.


11
Насправді 1.30 - 2*(0.65)це ідеальний приклад вираження, який, очевидно, оцінює 0,0, якщо ваш компілятор реалізує IEEE 754, тому що подвійні представлені як 0.65і 1.30мають однакові ознаки, а множення на два очевидно точно.
Паскаль Куок

7
Ще отримую реп із цього, тому я змінив фрагмент другого прикладу.
Марка високої продуктивності

22

Я хочу дати трохи іншу відповідь, ніж інші. Вони відмінно підходять для відповіді на ваше запитання, як заявлено, але, ймовірно, не для того, що вам потрібно знати, або яка ваша реальна проблема.

Плаваюча точка у графіці чудово! Але майже ніколи не потрібно порівнювати прямо поплавці. Навіщо вам це потрібно робити? Графіка використовує поплавці для визначення інтервалів. І порівняння, якщо поплавок знаходиться в інтервалі, також визначеному поплавцями, завжди добре визначене і просто повинно бути послідовним, а не точним або точним! Поки може бути призначений піксель (який також є інтервалом!) - це все, що потрібно для графіки.

Тож якщо ви хочете перевірити, чи ваша точка знаходиться за межами [0..width [діапазон], це просто добре. Просто переконайтеся, що ви послідовно визначаєте включення. Наприклад, завжди визначаємо всередині (x> = 0 && x <ширина). Те ж саме стосується перехрестя або тестових ударів.

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


13

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

Розмовляючи ідеально репрезентативними значеннями, ви отримуєте 24 біти діапазону в понятті потужності два (одна точність). Отже, 1, 2, 4 є ідеально представними, як і .5, .25 та .125. Поки всі ваші важливі біти знаходяться в 24 бітах, ви золоті. Так 10.625 можна точно представити.

Це чудово, але швидко розвалиться під тиском. На думку спадають два сценарії: 1) Коли йдеться про розрахунок. Не довіряйте цьому sqrt (3) * sqrt (3) == 3. Це просто не буде так. І, мабуть, це не буде в межах епсилону, як підказують деякі інші відповіді. 2) Якщо задіяний будь-який не-потужний 2 (NPOT). Тож це може здатися дивним, але 0,1 - це нескінченний ряд у двійковій формі, тому будь-який обчислення, що включає таке число, буде неточним з самого початку.

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


11

[Правильна відповідь "переливається над вибором K. Вибір в Kкінцевому підсумку є настільки ж спеціальним, як і вибір, VISIBLE_SHIFTале вибір Kменш очевидний, оскільки на відміну від VISIBLE_SHIFTнього не ґрунтується жодна властивість відображення. Таким чином, відбирайте отруту - вибирайте Kабо вибирайте VISIBLE_SHIFT. Ця відповідь виступає за вибір, VISIBLE_SHIFTа потім демонструє труднощі при виборі K]

Саме через круглі помилки не слід використовувати порівняння «точних» значень для логічних операцій. У вашому конкретному випадку позиції на візуальному дисплеї це не може мати значення, якщо позиція становить 0,0 або 0,0000000003 - різниця невидима для ока. Отже, ваша логіка повинна бути чимось на зразок:

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

Однак, зрештою, "невидимий для очей" буде залежати від ваших властивостей дисплея. Якщо ви можете на верхній межі дисплея (ви повинні мати можливість); то вибирайте VISIBLE_SHIFTчастку цієї верхньої межі.

Тепер "правильна відповідь" опирається, Kтому давайте вивчимо вибір K. Правильна відповідь вище говорить:

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

Тож нам потрібно K. Якщо отримати Kскладніше, менш інтуїтивно зрозуміло, ніж вибрати мою, VISIBLE_SHIFTто ви вирішите, що для вас працює. Щоб знайти, Kми збираємось написати тестову програму, яка розглядає купу Kзначень, щоб ми могли побачити, як вона поводиться. Має бути очевидним, як вибрати K, якщо "правильна відповідь" може бути використана. Ні?

Ми будемо використовувати як детальну інформацію про "правильну відповідь":

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

Давайте просто спробуємо всі значення 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

Так, K має бути 1e16 або більше, якщо я хочу, щоб 1e-13 було "нулем".

Отже, я б сказав, що у вас є два варіанти:

  1. Зробіть прості обчислення епсілону, використовуючи інженерне судження для значення "epsilon", як я запропонував. Якщо ви займаєтесь графікою, а «нуль» означає «видимі зміни», ніж вивчайте свої візуальні активи (зображення тощо) та судіть, яким може бути епсилон.
  2. Не намагайтеся проводити будь-які обчислення з плаваючою точкою, поки ви не прочитаєте посилання на відповідь, що не відповідає культу (і ви отримали докторську ступінь в процесі), а потім не використовуйте своє неінтуїтивне судження для вибору K.

10
Одним з аспектів незалежності рішення є те, що ви не можете точно сказати, що таке "видимий зсув" під час компіляції. Те, що невидиме на екрані супер HD, може бути очевидним на крихітному екрані. Треба хоча б зробити це залежно від розміру екрана. Або назвіть щось інше.
Ромен

1
Але принаймні вибір "видимого зсуву" базується на легко зрозумілих властивостях відображення (або кадру) - на відміну від <правильної відповіді>, Kяку важко та неінтуїтивно вибрати.
GoZoner

5

Правильне запитання: як можна порівняти бали у какао-дотику?

Правильна відповідь: CGPointEqualToPoint ().

Інше запитання: чи дві обчислені значення однакові?

Відповідь, розміщена тут: їх немає.

Як перевірити, чи вони поруч? Якщо ви хочете перевірити, чи вони близькі, не використовуйте CGPointEqualToPoint (). Але не перевіряйте, чи є вони поруч. Зробіть щось, що має сенс у реальному світі, як-от перевірити, чи є точка за межею лінії чи точка знаходиться у сфері.


4

Востаннє, коли я перевіряв стандарт С, не було вимоги, щоб операції з плаваючою комою на парних (64 біт, 53-бітна мантіса) були точнішими з більшою точністю. Однак деякі апаратні засоби можуть робити операції в регістрах більшої точності, і ця вимога тлумачиться так, що не вимагає очищення бітів нижчого порядку (за винятком точності чисел, що завантажуються в регістри). Таким чином, ви можете отримати несподівані результати таких порівнянь залежно від того, що залишилося в регістрах від того, хто останній там спав.

Це сказало, і, незважаючи на мої зусилля, щоб викреслити це, коли я бачу, в спорядженні, де я працюю, є багато коду С, який складається за допомогою gcc і працює на Linux, і ми не помітили жодного з цих несподіваних результатів протягом дуже тривалого часу . Я поняття не маю, чи це через те, що gcc очищає біти низького порядку для нас, 80-бітні регістри не використовуються для цих операцій на сучасних комп’ютерах, стандарт змінено чи що. Мені хотілося б знати, чи може хтось цитувати главу та вірш.


1

Ви можете використовувати такий код для порівняння float з нулем:

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

Це порівнятимемо з точністю 0,1, що достатньо для CGFloat в цьому випадку.


Кастинг intбез страхування theView.frame.origin.xзнаходиться в / біля цього діапазону, intпризводить до невизначеної поведінки (UB) - або в цьому випадку 1/100-го діапазону int.
chux

Немає абсолютно ніяких причин для перетворення на ціле число, як це. Як говорив chux, існує потенціал для UB від значень поза діапазону; а для деяких архітектур це буде значно повільніше, ніж просто робити обчислення у плаваючій точці. Нарешті, множення на 100 подібних буде порівнювати з точністю 0,01, а не 0,1.
Снефтель

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

Я використовую таку функцію порівняння, щоб порівняти ряд десяткових знаків:

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

Я б сказав, що правильне - оголосити кожне число об'єктом, а потім визначити три речі в цьому об'єкті: 1) оператор рівності. 2) метод setAcceptableDifference. 3) саме значення. Оператор рівності повертає істину, якщо абсолютна різниця двох значень менша за значення, встановлене як прийнятне.

Ви можете підкласифікувати об'єкт відповідно до проблеми. Наприклад, круглі металеві бруски розміром від 1 до 2 дюймів можна вважати рівним діаметром, якщо їх діаметри відрізнялися менше ніж 0,0001 дюйма. Тож ви б закликали setAcceptableDifference з параметром 0.0001, а потім впевнено використовували оператор рівності.


1
Це не хороша відповідь. По-перше, вся "об'єктна річ" нічого не робить для вирішення вашої проблеми. По-друге, ваша реальна реалізація "рівності" насправді не є правильною.
Том Свірлі

3
Томе, можливо, ти знову подумаєш про "предметну річ". З реальними числами, представленими з високою точністю, рідкість буває рідко. Але ідея рівності може бути розроблена, якщо вона вам підходить. Було б приємніше, якби був оператор, який може бути замінений на "приблизно рівний", але його немає.
Джон Уайт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.