Чому додавання 0,1 декількох разів залишається без втрат?


152

Я знаю, що 0.1десяткове число не може бути представлене точно кінцевим двійковим числом ( пояснення ), тому double n = 0.1втратить деяку точність і не буде точно 0.1. З іншого боку 0.5може бути представлена ​​саме тому, що вона є 0.5 = 1/2 = 0.1b.

Сказавши, що зрозуміло, що додавання 0.1 три рази не дасть точно 0.3так, наступні відбитки коду false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Але тоді як це, що додавання в 0.1 п’ять разів дасть точно 0.5? Виводиться наступний код true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Якщо 0.1неможливо точно представити, як це, якщо додавання його в 5 разів дає саме те, 0.5що можна точно представити?


7
Якщо ви справді це досліджуєте, я впевнений, що ви можете це зрозуміти, але плаваюча точка навантажена "сюрпризами", а іноді краще просто дивитися на диво.
Гарячі лизання

3
Ви думаєте про це по-математичному. Арифметика з плаваючою комою ніяк не математика.
Якоб

13
@HotLicks - це дуже неправильне ставлення.
варення

2
@RussellBorogove, навіть якщо його було оптимізовано, це було б справедливою оптимізацією лише в тому випадку, якщо sumбуло б те саме кінцеве значення, як якщо б цикл був справді виконаний. У стандарті C ++ це називається "правилом як би" або "такою ж поведінкою, що спостерігається".
варення

7
@Jakob зовсім не вірна. Арифметика з плаваючою комою суворо визначена, з хорошою математичною обробкою меж помилок і подібних. Просто багато програмістів або не бажають слідкувати за аналізом, або вони помилково вважають, що "плаваюча точка є неточною" - це все, що потрібно знати, і з цим аналізом не варто займатись.
варення

Відповіді:


155

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

Так , наприклад 0.1, неточно , 0.1тобто , new BigDecimal("0.1") < new BigDecimal(0.1)але 0.5це точно1.0/2

Ця програма показує вам справжні цінності.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

відбитки

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Зауважте: 0.3це трохи вимкнено, але коли ви дістаєтесь до 0.4бітів, вам доведеться змістити його вниз, щоб вписатись у 53-бітний ліміт, і помилка відкидається. Знову ж , закрадається помилка назад протягом 0.6і 0.7але 0.8до 1.0відкидається помилка.

Додаючи його 5 разів, слід накопичувати помилку, а не її скасовувати.

Причина помилки пов’язана з обмеженою точністю. тобто 53-біт. Це означає, що при збільшенні кількості бітів у міру їх збільшення біти повинні бути викинуті з кінця. Це спричиняє округлення, яке в даному випадку вам на користь.
Ви можете отримати зворотний ефект, отримавши меншу кількість, наприклад 0.1-0.0999=>, 1.0000000000000286E-4 і ви побачите більше помилок, ніж раніше.

Прикладом цього є те, чому в Java 6 Чому Math.round (0.49999999999999994) повертається 1 У цьому випадку втрата біта в обчисленні призводить до великої різниці у відповіді.


1
Де це реалізовано?
EpicPandaForce

16
@ Zhuinden Процесор відповідає стандарту IEEE-754. Java надає вам доступ до основних інструкцій процесора і не бере участь у цьому. en.wikipedia.org/wiki/IEEE_floating_point
Пітер Лорі

10
@PeterLawrey: Не обов’язково процесор. На машині без плаваючої точки в процесорі (і не використовується окремий FPU), арифметика IEEE буде виконуватися програмним забезпеченням. І якщо хост-процесор має плаваючу крапку, але він не відповідає вимогам IEEE, я думаю, що реалізація Java для цього процесора також зобов’язана буде використовувати soft float ...
R .. GitHub STOP HELPING ICE

1
@R .. в такому випадку я не знаю, що трапилося б, якщо ви використали strictfp час, щоб розглянути цілі числа з фіксованою точкою, я думаю. (або BigDecimal)
Пітер Лорі

2
@eugene ключовою проблемою є обмежена величина, яку може представляти плаваюча точка. Це обмеження може призвести до втрати інформації, і коли число зростає, втрата помилки. Він використовує округлення, але в цьому випадку округляє вниз, тому що було б число, яке занадто велике, а 0,1 трохи завелике, перетворюється на правильне значення. Рівно 0,5
Пітер Лорі

47

Переповнення заборони в плаваючій точці - x + x + xце точно правильно округлене (тобто найближче) число з плаваючою точкою до реального 3 * x, x + x + x + xрівно 4 * x, і x + x + x + x + xзнову є правильно округлене наближення з плаваючою точкою для 5 * x.

Перший результат, бо x + x + xвипливає з того, що x + xє точним. x + x + xТаким чином, це результат лише одного округлення.

Другий результат більш важким, одна демонстрація цього обговорюється тут (і Стівен Canon посилається на інший доказ у справі аналізу на останні 3 цифри x). Підводячи підсумок, або 3 * xзнаходиться в тому ж бінаді, що і 2 *, xабо знаходиться в тому ж бінаді, що і 4 * x, і в кожному конкретному випадку можна зробити висновок, що помилка на третьому доповненні скасовує помилку на другому додаванні ( перше доповнення точно, як ми вже говорили).

Третій результат, « x + x + x + x + xправильно округлений», випливає з другого так само, як перший походить від точності x + x.


Другий результат пояснює, чому 0.1 + 0.1 + 0.1 + 0.1саме це число з плаваючою комою 0.4: раціональні числа 1/10 і 4/10 наближаються однаково, з однаковою відносною помилкою, при перетворенні на плаваючу крапку. Ці числа з плаваючою комою мають співвідношення рівно 4 між ними. Перший і третій результати показують, що 0.1 + 0.1 + 0.1і, як 0.1 + 0.1 + 0.1 + 0.1 + 0.1можна очікувати, буде мати меншу кількість помилок, ніж це може бути зроблено шляхом аналізу наївних помилок, але самі по собі вони лише співвідносять результати відповідно 3 * 0.1і 5 * 0.1, які, як можна очікувати, будуть близькими, але не обов'язково ідентичними 0.3і 0.5.

Якщо ви продовжуєте додавати і 0.1після четвертого додавання, ви, нарешті, помітите помилки округлення, які роблять « 0.1додані до себе n разів» відхилення n * 0.1та розбігаються ще більше від n / 10. Якби ви побудували графіки значень "0,1, доданих до себе n разів", як функцію n, ви б спостерігали лінії постійного нахилу бінадами (як тільки результат n-го додавання призначений потрапити в певний бінад, властивості додавання можна очікувати аналогічно попереднім доповненням, що призвели до результату в тому самому бінаді). У межах одного бінада помилка або зростатиме, або зменшується. Якби ви подивилися на послідовність схилів від бінади до бінади, ви б розпізнали повторювані цифри0.1у двійковому на деякий час. Після цього почне поглинання, і крива піде плоскою.


1
У першому рядку ви говорите, що x + x + x є абсолютно правильним, але з прикладу в питанні це не так.
Альбоз

2
@Alboz Я кажу, що x + x + xце точно правильно округлене число з плаваючою комою до реального 3 * x. "Правильно округлений" означає "найближчий" в цьому контексті.
Паскаль Куок

4
+1 Це має бути прийнятою відповіддю. Він насправді пропонує пояснення / докази того, що відбувається, а не просто розпливчасті загальні риси.
R .. GitHub СТОП ДОПОМОГА ВІД

1
@Alboz (все це передбачено питанням). Але пояснення цієї відповіді полягає в тому, як помилки випадково скасовуються, а не додаються в гіршому випадку.
варення

1
@chebus 0,1 дорівнює 0x1,999999999999999999999… p-4 у шістнадцятковій формі (нескінченна послідовність цифр). Він апроксимований у подвійній точності як 0x1.99999ap-4. 0,2 - це 0x1,999999999999999999999… p-3 у шістнадцятковій кількості. З тієї ж причини, що 0,1 наближено як 0x1,99999ap-4, 0,2 наближено як 0x1,99999ap-3. Тим часом 0x1,99999ap-3 також рівно 0x1,99999ap-4 + 0x1,99999ap-4.
Паскаль Куок

-1

Системи з плаваючою точкою роблять різні магії, включаючи кілька додаткових біт точності для округлення. Таким чином, дуже мала помилка через неточне подання 0,1 закінчується округленням до 0,5.

Подумайте про те, що плаваюча точка є чудовим, але НЕЗАКОННИМ способом представлення чисел. Не всі можливі числа легко представлені в комп'ютері. Ірраціональні числа, такі як PI. Або як SQRT (2). (Символічні математичні системи можуть їх представляти, але я сказав "легко".)

Значення плаваючої точки може бути надзвичайно близьким, але не точним. Це може бути настільки близько, що ви могли б перейти до Плутона і відійти на міліметри. Але все ще не точно в математичному сенсі.

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

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

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

Для підрахунку додатків (включаючи бухгалтерський облік) використовуйте цілі числа. Для підрахунку кількості людей, які проходять через ворота, використовуйте int або long.


2
Питання позначено тегом [java]. Визначення мови Java не передбачає "декілька зайвих біт точності", лише декілька зайвих бітів експоненти (і це лише в тому випадку, якщо ви не використовуєте strictfp). Тільки тому, що ви відмовилися від розуміння чогось, це не означає, що це неможливо і навіть не варто відмовлятися від інших. Дивіться stackoverflow.com/questions/18496560 як приклад тривалості реалізації Java, щоб реалізувати визначення мови (яке не включає жодних положень щодо бітів додаткової точності, а також strictfpдля будь-якого додаткового біта exp)
Pascal Cuoq
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.