TL; DR
- Використовуйте наведену нижче функцію замість прийнятого в даний час рішення, щоб уникнути небажаних результатів у певних обмежених випадках, але бути потенційно більш ефективним.
- Знайте очікувану неточність ваших цифр і відповідно подайте їх у функції порівняння.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
Графіка, будь ласка?
При порівнянні чисел з плаваючою комою існує два "режими".
Перший - це відносний режим, де різниця між xі yрозглядається відносно їх амплітуди |x| + |y|. Якщо побудувати графік у 2D, це дає такий профіль, де зелений означає рівність xі y. (Я взяв epsilon0,5 для ілюстрації).

Відносний режим - це те, що використовується для "нормальних" або "досить великих" значень з плаваючою комою. (Докладніше про це пізніше).
Другий - це абсолютний режим, коли ми просто порівнюємо їх різницю з фіксованим числом. Він дає наступний профіль (знову ж epsilonіз 0,5 і relth1 для ілюстрації).

Цей абсолютний режим порівняння є тим, що використовується для "крихітних" значень із плаваючою комою.
Тепер питання полягає в тому, як ми з’єднаємо ці дві моделі відповіді.
У відповіді Майкла Боргвардта перехід базується на значенні diff, яке повинно бути нижче relth( Float.MIN_NORMALу його відповіді). Ця зона перемикання показана у вигляді штрихування на графіку нижче.

Оскільки relth * epsilonменший за розмір relth, зелені плями не злипаються, що, в свою чергу, надає рішенню погану властивість: ми можемо знайти такі триплети чисел, що x < y_1 < y_2і все ж, x == y2але x != y1.

Візьмемо цей яскравий приклад:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
Ми маємо x < y1 < y2, і насправді y2 - xбільше, ніж у 2000 разів y1 - x. І все-таки з поточним рішенням,
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
На відміну від цього, у запропонованому вище рішенні зона перемикання базується на значенні |x| + |y|, яке представлене штриховим квадратом внизу. Це гарантує, що обидві зони витончено з'єднуються.

Крім того, код вище не має розгалуження, що може бути більш ефективним. Враховуйте, що такі операції, як maxі abs, які апріорі потребують розгалуження, часто мають спеціальні інструкції зі складання. З цієї причини, я думаю, що цей підхід перевершує інше рішення, яке полягало б у виправленні Майкла nearlyEqualшляхом зміни перемикача з diff < relthна diff < eps * relth, що в подальшому дало б по суті той самий шаблон відповіді.
Де перемикатися між відносним та абсолютним порівнянням?
Перемикання між цими режимами здійснюється навколо relth, що приймається як FLT_MINу прийнятій відповіді. Цей вибір означає, що подання - float32це те, що обмежує точність наших чисел із плаваючою комою.
Це не завжди має сенс. Наприклад, якщо числа, які ви порівнюєте, є результатами віднімання, можливо, щось із діапазону FLT_EPSILONмає більше сенсу. Якщо це квадрати коренів від відніманих чисел, числова неточність може бути ще вищою.
Це досить очевидно, коли ви розглядаєте порівняння плаваючої крапки з 0. Тут будь-яке відносне порівняння не вдасться, оскільки |x - 0| / (|x| + 0) = 1. Отже, порівняння повинно перейти в абсолютний режим, коли xвідбувається порядок неточності вашого обчислення - і рідко воно буває настільки низьким, як FLT_MIN.
Це є причиною введення relthпараметра вище.
Крім того, не помножуючи relthна epsilon, інтерпретація цього параметра є простою і відповідає рівню числової точності, який ми очікуємо щодо цих чисел.
Математичне бурчання
(зберігається тут здебільшого для власного задоволення)
У більш загальному плані я припускаю, що добре поведений оператор порівняння з плаваючою точкою =~повинен мати деякі основні властивості.
Далі досить очевидно:
- саморівність:
a =~ a
- симетрія:
a =~ bпередбачаєb =~ a
- інваріантність проти опозиції:
a =~ bмається на увазі-a =~ -b
(У нас немає a =~ bі b =~ cмається на увазі a =~ c, =~це не відношення еквівалентності).
Я хотів би додати такі властивості, які є більш специфічними для порівняння з плаваючою комою
- якщо
a < b < c, то a =~ cмається на увазіa =~ b (ближчі значення також повинні бути рівними)
- якщо
a, b, m >= 0тоді a =~ bмає на увазі a + m =~ b + m(більші значення з однаковою різницею також повинні бути рівними)
- якщо
0 <= λ < 1тоді це a =~ bпередбачає λa =~ λb(можливо, менш очевидний аргумент за).
Ці властивості вже дають сильні обмеження щодо можливих функцій майже рівності. Запропонована вище функція перевіряє їх. Можливо, відсутня одна або декілька очевидних властивостей.
Коли один думати , =~як сімейство відносин рівності =~[Ɛ,t]параметризрвані Ɛі relth, можна також додати
- якщо
Ɛ1 < Ɛ2тоді a =~[Ɛ1,t] bмається на увазі a =~[Ɛ2,t] b(рівність для даного допуску передбачає рівність при вищій допуску)
- якщо
t1 < t2тоді a =~[Ɛ,t1] bмається на увазі a =~[Ɛ,t2] b(рівність для даної неточності передбачає рівність при вищій точності)
Запропоноване рішення також підтверджує їх.