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
. (Я взяв epsilon
0,5 для ілюстрації).
Відносний режим - це те, що використовується для "нормальних" або "досить великих" значень з плаваючою комою. (Докладніше про це пізніше).
Другий - це абсолютний режим, коли ми просто порівнюємо їх різницю з фіксованим числом. Він дає наступний профіль (знову ж epsilon
із 0,5 і relth
1 для ілюстрації).
Цей абсолютний режим порівняння є тим, що використовується для "крихітних" значень із плаваючою комою.
Тепер питання полягає в тому, як ми з’єднаємо ці дві моделі відповіді.
У відповіді Майкла Боргвардта перехід базується на значенні 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
(рівність для даної неточності передбачає рівність при вищій точності)
Запропоноване рішення також підтверджує їх.