Перш за все, значення з плаваючою комою не є "випадковими" у своїй поведінці. Точне порівняння може і має сенс у багатьох реальних звичках. Але якщо ви збираєтеся використовувати плаваючу крапку, вам потрібно знати, як вона працює. Помилка з боку припущення, що плаваюча точка працює як реальні числа, ви отримаєте код, який швидко порушується. Помилка з боку припущення, що результати з плаваючою комою мають великі випадкові нечіткі зв'язки з ними (як і більшість відповідей, що тут пропонують), ви отримаєте код, який, здається, працює спочатку, але в кінцевому підсумку має помилки великої величини та розбиті кутові випадки.
Перш за все, якщо ви хочете програмувати з плаваючою точкою, слід прочитати це:
Що повинен знати кожен вчений-комп’ютер про арифметику з плаваючою комою
Так, прочитайте все це. Якщо це занадто велике навантаження, слід використовувати цілі числа / фіксовану точку для своїх розрахунків, поки не встигнете її прочитати. :-)
Тепер, з огляду на це, найбільші проблеми з точним порівнянням плаваючої точки зводиться до:
Той факт, що багато значень, які ви можете записати у джерело, чи прочитати з scanf
або strtod
, не існують як значення з плаваючою комою та мовчки перетворюються на найближче наближення. Про це йшлося у відповіді demon9733.
Той факт, що багато результатів округляються через недостатню точність, щоб представити фактичний результат. Простий приклад, коли ви можете бачити це - додавання x = 0x1fffffe
та y = 1
плавання. Тут x
є 24 біта точності в мантісі (гаразд) і y
має всього 1 біт, але коли ви додаєте їх, їх біти знаходяться не в місцях, що перекриваються, і в результаті знадобиться 25 біт точності. Натомість він стає округленим (до 0x2000000
режиму округлення за замовчуванням).
Той факт, що багато результатів округлюються через необхідність нескінченно багато місць для правильного значення. Це включає в себе як раціональні результати, такі як 1/3 (які ви знайомі з десяткової, де вона займає нескінченно багато місць), а також 1/10 (що також займає нескінченно багато місць у двійковій формі, оскільки 5 не є силою 2), а також нераціональні результати, такі як квадратний корінь усього, що не є ідеальним квадратом.
Подвійне округлення. У деяких системах (зокрема x86) вирази з плаваючою комою оцінюються з більшою точністю, ніж їх номінальні типи. Це означає, що коли трапиться один із перерахованих вище типів округлення, ви отримаєте два етапи округлення, спочатку округлення результату до типу більш точної, потім округлення до кінцевого типу. Як приклад, розглянемо, що відбувається у десятковій формі, якщо округлити 1,49 до цілого числа (1), а порівняно з тим, що спочатку округлите його до одного десяткового знаку (1,5), а потім округлите цей результат до цілого числа (2). Це насправді одна з найнебезпечніших областей, з якою потрібно боротися в плаваючій точці, оскільки поведінка компілятора (особливо для помилкових, невідповідних компіляторів, як GCC) непередбачувана.
Трансцендентні функції ( trig
, exp
, log
і т.д.) не визначені , щоб правильно округлені результати; результат вказується просто правильним у межах однієї одиниці в останньому місці точності (зазвичай це називається 1ulp ).
Коли ви пишете код з плаваючою комою, вам потрібно пам’ятати, що ви робите з числами, які можуть призвести до неточності результатів, і робити порівняння відповідно. Часто буває сенс порівнювати з "епсилоном", але цей епсилон повинен базуватися на величині чисел, які ви порівнюєте , а не на абсолютній постійній. (У випадках, коли абсолютний постійний епсілон спрацював би, це абсолютно вказує на те, що фіксована точка, а не плаваюча точка є правильним інструментом для роботи!)
Редагувати: Зокрема, перевірка епсилону відносної величини повинна виглядати приблизно так:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))
Звідки FLT_EPSILON
константа з float.h
(замініть її DBL_EPSILON
на double
s або LDBL_EPSILON
для long double
s) і K
є константою, ви виберете таке, що накопичена помилка ваших обчислень напевно обмежена K
одиницями на останньому місці (і якщо ви не впевнені, що ви отримали помилку правильний розрахунок, зробіть K
в кілька разів більше, ніж те, що ваші розрахунки говорять, що має бути).
Нарешті, зауважте, що якщо ви користуєтесь цим, може знадобитися якийсь особливий догляд поблизу нуля, оскільки FLT_EPSILON
не має сенсу для денноматок. Швидке виправлення було б зробити:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)
і аналогічно замінити, DBL_MIN
якщо використовувати парні.
fabs(x+y)
проблематично, якщоx
іy
(може) мати різні ознаки. І все-таки хороша відповідь проти припливу вантажно-культових порівнянь.