Нерівність, спричинена неточністю поплавця


15

Принаймні на Java, якщо я напишу цей код:

float a = 1000.0F;
float b = 0.00004F;
float c = a + b + b;
float d = b + b + a;
boolean e = c == d;

значення було б . Я вважаю, що це викликано тим, що поплавці дуже обмежені в точному зображенні чисел. Але я не розумію , чому просто змінити положення може викликати ця нерівність.efalsea

Я зменшив s до одного в обох рядках 3 і 4, як показано нижче, значення однак стає :betrue

float a = 1000.0F;
float b = 0.00004F;
float c = a + b;
float d = b + a;
boolean e = c == d;

Що саме сталося у рядках 3 та 4? Чому операції додавання з плавцями не асоціативні?

Заздалегідь спасибі.


16
Як показує ваш приклад, додавання з плаваючою комою є комутативним. Але це не асоціативно.
Yuval Filmus

1
Я закликаю вас переглянути основні визначення вгору. Зауважте також, що компілятор аналізує як ( r + s ) + t (додавання асоціюється зліва). r+s+t(r+s)+t
Yuval Filmus

2
Щоб легко зрозуміти, чому це повинно бути так, розглянемо Xдуже велику кількість і Yдуже малу кількість, таку, що X + Y = X. Тут X + Y + -Xбуде нуль. Але X + -X + Yбуде Y.
Девід Шварц


Відповіді:


20

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

Порівняйте і b + a : Результат кожної операції, виконаної з нескінченною точністю, однаковий, тому ці однакові результати безмежної точності округлюються однаково. Іншими словами, додавання з плаваючою комою є комутативним.a+bb+a

Візьміть : b - число з плаваючою комою. З двійковими числами з плаваючою точкою 2 b також є числом з плаваючою комою (показник більший на одиницю), тому b + b додається без жодної помилки округлення. Тоді а додається до точного значення b + b . Результат - точне значення 2 b + a , округлене до найближчого числа з плаваючою комою.b+b+ab2bb+bab+b2b+a

Візьміть : додається a + b , і буде помилка округлення r , тому отримаємо результат a + b + r . Додайте b , і результат - точне значення 2 b + a + r , округлене до найближчого числа з плаваючою комою.a+b+ba+bra+b+rb2b+a+r

Так в одному випадку , округлий. В іншому випадку 2 b + a + r , округлий.2b+a2b+a+r

PS. Чи буде для двох конкретних чисел і b обидва обчислення давати однаковий результат чи ні, залежить від чисел та помилки округлення в обчисленні a + b , і зазвичай важко передбачити. Використання одинарної або подвійної точності принципово не має значення для проблеми, але оскільки похибки округлення відрізняються, будуть значення a і b, де в одній точності результати рівні, а в подвійній точності вони не є, або навпаки. Точність буде набагато вищою, але проблема в тому, що два вирази математично однакові, але не однакові в арифметиці з плаваючою комою залишаються однаковими.aba+b

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

PPPS. Коментар запитав, чи варто запитувати, чи рівні номери з плаваючою комою рівні чи взагалі немає. Абсолютно, якщо ти знаєш, що робиш. Наприклад, якщо ви сортуєте масив або реалізуєте набір, ви потрапите в жахливі проблеми, якщо хочете використовувати якесь поняття "приблизно рівне". У графічному інтерфейсі користувача вам може знадобитися перерахувати розміри об'єктів, якщо розмір об'єкта змінився - ви порівнюєте oldSize == newSize, щоб уникнути перерахунку, знаючи, що на практиці ви майже ніколи не маєте майже однакових розмірів, і ваша програма правильна навіть якщо є непотрібний перерахунок.


У цьому конкретному випадку b стає періодичним при перетворенні на двійкові, тому скрізь є помилки округлення.
Андре Соуза Лемос

1
@ AndréSouzaLemos bу цій відповіді не 0,00004, це те, що ви отримуєте після конверсії та округлення.
Олексій Романов

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

Наївне запитання: чи має сенс тестування рівності на поплавці? Чому більшість мов програмування дозволяють перевірити aa == b, де обидва або одна є плаваючою?
curious_cat

Відповідне визначення з Вікіпедії: " Machine Epsilon дає верхню межу відносної похибки через округлення в арифметиці з плаваючою комою".
Blackhawk

5

Бінарний формат з плаваючою комою, підтримуваний комп'ютерами, по суті схожий на десяткову наукову нотацію, яку використовують люди.

Число з плаваючою комою складається з знака, мантіси (фіксованої ширини) і експонента (фіксованої ширини), як це:

+/-  1.0101010101 × 2^12345
sign   ^mantissa^     ^exp^

Звичайний науковий запис має подібний формат:

+/- 1.23456 × 10^99

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


Приклад

Для ілюстрації, припустимо, ми використовуємо рівно 3 цифри після десяткової крапки.

a = 99990 = 9.999 × 10^4
b =     3 = 3.000 × 10^0

(a + b) + b

Тепер ми обчислюємо:

c = a + b
  = 99990 + 3      (exact)
  = 99993          (exact)
  = 9.9993 × 10^4  (exact)
  = 9.999 × 10^4.  (rounded to nearest)

На наступному кроці, звичайно:

d = c + b
  = 99990 + 3 = ...
  = 9.999 × 10^4.  (rounded to nearest)

Звідси (a + b) + b = 9,999 × 10 4 .

(b + b) + a

Але якщо ми зробили операції в іншому порядку:

e = b + b
  = 3 + 3  (exact)
  = 6      (exact)
  = 6.000 × 10^0.  (rounded to nearest)

Далі ми обчислюємо:

f = e + a
  = 6 + 99990      (exact)
  = 99996          (exact)
  = 9.9996 × 10^4  (exact)
  = 1.000 × 10^5.  (rounded to nearest)

Звідси (b + b) + a = 1.000 × 10 5 , що відрізняється від нашої іншої відповіді.


5

Java використовує бінарне представлення з плаваючою комою IEEE 754, яке присвячує 23 двійкових цифр мантісі, що нормалізується для початку з першої значної цифри (пропущено, щоб заощадити місце).

0,0000410=0,00000000000000101001111100010110101100010001110001101101000111 ...2=[1.]01001111100010110101100010001110001101101000111 ...2×2-15

100010+0,0000410=1111101000.00000000000000101001111100010110101100010001110001101101000111 ...2=[1.]11110100000000000000000101001111100010110101100010001110001101101000111...2×29

Червоні частини - це мантіси, оскільки вони фактично представлені (до округлення).

(100010+0.0000410)+0.0000410(0.0000410+0.0000410)+100010


0

Нещодавно ми зіткнулися з подібною проблемою округлення. Вищезгадані відповіді є правильними, проте досить технічними.

Я знайшов таке, що є хорошим поясненням того, чому існують помилки округлення. http://csharpindepth.com/Articles/General/FloatingPoint.aspx

TLDR: двійкові плаваючі точки не можуть бути точно відображені в десяткових плаваючих точках. Це викликає неточності, які можуть скластися під час математичних операцій.

Приклад з використанням десяткових плаваючих чисел: 1/3 + 1/3 + 1/3 зазвичай дорівнює 1. Однак у десяткових колах: 0,333333 + 0,333333 + 0,333333 ніколи точно не дорівнює 1.000000

Те саме відбувається при виконанні математичних операцій над двійковими десятковістю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.