Чи порушена математика з плаваючою комою?


2982

Розглянемо наступний код:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Чому трапляються ці неточності?


127
Змінні з плаваючою комою зазвичай мають таку поведінку. Це викликано тим, як вони зберігаються в апаратному забезпеченні. Для отримання додаткової інформації дивіться статтю Вікіпедії про числа з плаваючою комою .
Бен С

62
JavaScript розглядає десяткові знаки як числа з плаваючою комою , що означає, що такі операції, як додавання, можуть зазнати помилки округлення. Ви можете поглянути на цю статтю: Що повинен знати кожен комп'ютерний вчений про арифметику з плаваючою комою
matt b

4
Для отримання інформації, ВСІ числові типи в javascript - IEEE-754.
Gary Willoughby

6
Оскільки JavaScript використовує стандарт IEEE 754 для Math, він використовує 64-бітні плаваючі числа. Це спричиняє помилки точності при виконанні обчислень з плаваючою комою (десяткові), коротше кажучи, через комп'ютери, що працюють в Базі 2, а десяткова - База 10 .
Pardeep Jain

Відповіді:


2250

Двійкова математика з плаваючою комою така. У більшості мов програмування він базується на стандарті IEEE 754 . Суть проблеми полягає в тому, що числа представлені в такому форматі як ціле число, що перевищує потужність двох; раціональні числа (наприклад 0.1, що є 1/10), знаменник яких не є силою двох, не можуть бути точно представлені.

Для 0.1стандартного binary64формату представлення може бути записане точно так само

  • 0.1000000000000000055511151231257827021181583404541015625 у десятковій або
  • 0x1.999999999999ap-4в C99 hexfloat нотації .

На відміну від цього, раціональне число 0.1, яке є 1/10, можна записати саме так

  • 0.1 у десятковій або
  • 0x1.99999999999999...p-4в аналозі позначення шестигранних пластів С99, де ...являє собою нескінченну послідовність 9-х.

Константи 0.2і 0.3у вашій програмі також будуть наближеними до їх справжніх значень. Буває, що найближчий doubleдо 0.2більший за раціональне число, 0.2але найближчий doubleдо 0.3менший за раціональне число 0.3. Сума 0.1та 0.2звивистість перевищує раціональне число, 0.3а значить, не погоджуючись з постійною у вашому коді.

Досить всебічне трактування питань арифметики з плаваючою комою - це те, що повинен знати кожен арифметик з плаваючою комою . Для легшого засвоєння пояснення див. Floating-point-gui.de .

Бічна примітка: Усі позиційні (базові N) системи з точністю поділяють цю проблему з точністю

Прості старі десяткові (базові 10) числа мають однакові питання, через що числа типу 1/3 закінчуються як 0,333333333 ...

Ви просто натрапили на число (3/10), яке може бути легко представити десятковою системою, але не підходить для двійкової системи. Це іде і в обидва напрямки (в деякій мірі): 1/16 - це некрасиве число в десятковій частині (0,0625), але у двійковій формі воно виглядає так само акуратно, як 10 000-е в десятковій (0,0001) ** - якби ми були в звичка використовувати систему чисел базової-2 у повсякденному житті, ви навіть подивитесь на це число і інстинктивно зрозумієте, що можете приїхати туди, вдвічі зменшивши його, вдвічі зменшивши її, знову і знову.

** Звичайно, це не зовсім те, як числа з плаваючою комою зберігаються в пам'яті (вони використовують форму наукової нотації). Однак це ілюструє те, що двійкові помилки точності з плаваючою комою, як правило, з'являються, оскільки "реальні" цифри, з якими ми зазвичай зацікавлені в роботі, настільки часто мають десять - але лише тому, що ми використовуємо систему десяткових чисел day- на сьогоднішній день. Ось чому ми скажемо такі речі, як 71% замість "5 з кожні 7" (71% - це наближення, оскільки 5/7 не може бути представлено точно жодним десятковим числом).

Так що ні: двійкові числа з плаваючою комою не порушені, вони просто виявляються такими ж недосконалими, як і всі інші базові системи N чисел :)

Бічна сторона Примітка: Робота з поплавками в програмуванні

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

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

Як НЕ робитиif (x == y) { ... }

Натомість робити if (abs(x - y) < myToleranceValue) { ... }.

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


181
Я думаю, що "деяка постійна помилка" є більш правильною, ніж "Епсілон", оскільки немає "Епсілона", який би міг використовуватися у всіх випадках. Різні епілони потрібно використовувати в різних ситуаціях. А машина epsilon майже ніколи не є доброю константою для використання.
Ротсор

34
Не зовсім вірно, що вся математика з плаваючою комою базується на стандарті IEEE [754]. Є ще деякі системи, які використовуються, які мають, наприклад, стару шістнадцяткову FP, і все ще є графічні карти, які не підтримують арифметику IEEE-754. Однак це правда з розумним наближенням.
Стівен Канон

19
Cray зрізав відповідність IEEE-754 за швидкість. Java також послабила свою прихильність як оптимізацію.
Art Taylor

28
Я думаю, вам слід додати щось до цієї відповіді про те, як слід завжди робити обчислення грошей, завжди робити арифметику з фіксованою точкою на цілі числа , оскільки гроші квантовані. (Можливо, має сенс робити обчислення внутрішнього обліку в невеликих частках відсотка, або будь-яка ваша найменша валютна одиниця - це часто допомагає, наприклад, зменшити помилку округлення при перерахуванні "$ 29,99 на місяць" на денну ставку - але це повинно все ще бути арифметикою з фіксованою точкою.)
zwol

18
Цікавий факт: цей самий 0,1 не точно представлений у двійковій плаваючій точці спричинив сумнозвісну помилку ракетного програмного забезпечення Patriot, в результаті якої в першій війні в Іраку загинули 28 людей.
hdl

602

Перспектива дизайнера обладнання

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

1. Огляд

З інженерної точки зору, більшість операцій з плаваючою комою матиме деякий елемент помилки, оскільки обладнання, яке виконує обчислення з плаваючою комою, вимагає лише помилки менше половини однієї одиниці в останньому місці. Таким чином, багато обладнання зупиняється на точності, необхідній лише для отримання помилки менше половини однієї одиниці в останньому місці для однієї операції, що особливо проблематично при поділі з плаваючою комою. Що становить одну операцію, залежить від того, скільки операндів приймає одиниця. Для більшості це два, але деякі одиниці приймають 3 і більше операндів. Через це не існує гарантії, що повторні операції призведуть до бажаної помилки, оскільки помилки накопичуються з часом.

2. Стандарти

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

У стандарті IEEE-754 дизайнерам апаратних засобів допускається будь-яке значення помилки / epsilon до тих пір, поки на останньому місці це менше половини однієї одиниці, і результат повинен бути менше половини однієї одиниці в останньому місці місце для однієї операції. Це пояснює, чому при повторних операціях помилки складаються. Для подвійної точності IEEE-754 - це 54-й біт, оскільки 53 біти використовуються для представлення числової частини (нормалізованої), також називається мантіси, числа плаваючої точки (наприклад, 5.3 в 5.3e5). У наступних розділах детальніше описуються причини апаратних помилок різних операцій з плаваючою точкою.

3. Причина помилки округлення в підрозділі

Основна причина помилки при поділі з плаваючою комою - алгоритми поділу, що використовуються для обчислення коефіцієнта. Більшість комп'ютерних систем обчислюють поділ, використовуючи множення на обернене, головним чином на Z=X/Y,Z = X * (1/Y). Ділення обчислюється ітераційно, тобто кожен цикл обчислює деякі біти коефіцієнта до досягнення бажаної точності, що для IEEE-754 є останньою помилкою, меншою за одну одиницю. Таблиця зворотних зворотів Y (1 / Y) відома як таблиця вибору коефіцієнтів (QST) у повільному поділі, а розмір у бітах таблиці відбору коефіцієнтів, як правило, є шириною радіусу або кількох бітів коефіцієнт, обчислений у кожній ітерації, плюс кілька захисних біт. Для стандарту IEEE-754, подвійна точність (64-розрядна), це був би розмір радіусу дільника, плюс декілька захисних біт k, де k>=2. Так, наприклад, типова таблиця вибору коефіцієнта для дільника, що обчислює 2 біти коефіцієнта одночасно (радіус 4), буде 2+2= 4бітами (плюс кілька необов'язкових біт).

3.1 Помилка округлення підрозділу: наближення взаємної

Які взаємні дані в таблиці вибору коефіцієнтів залежать від методу поділу : повільний поділ, такий як поділ СТО, або швидкий поділ, такий як поділ Гольдшмідта; кожен запис модифікується відповідно до алгоритму поділу, намагаючись отримати мінімально можливу помилку. У будь-якому випадку, все взаємні відповіді є наближеннямифактичної взаємної і ввести деякий елемент помилки. І методи повільного ділення, і швидке ділення обчислюють коефіцієнт ітеративно, тобто деяку кількість бітів коефіцієнта обчислюють кожен крок, потім результат віднімають з дивіденду, і дільник повторює кроки, поки помилка не буде меншою половини одиниці одиниця в останньому місці. Методи повільного ділення обчислюють фіксовану кількість цифр коефіцієнта на кожному кроці і, як правило, є менш дорогими для побудови, а швидкі методи поділу обчислюють змінну кількість цифр за крок і, як правило, дорожче будувати. Найважливіша частина методів поділу полягає в тому, що більшість з них покладається на повторне множення наближенням зворотного зв'язку, тому вони схильні до помилок.

4. Помилки округлення в інших операціях: усічення

Ще однією причиною помилок округлення у всіх операціях є різні режими усікання остаточної відповіді, які дозволяє IEEE-754. Існують усічення, «круглий бік» до «нуля», « круглий до найближчого» (за замовчуванням), « округлення» та «округлення». Усі методи вводять елемент помилки менше однієї одиниці в останню чергу за одну операцію. З часом і повторних операцій усічення також додає кумулятивно до отриманої помилки. Ця помилка усікання є особливо проблематичною при експоненції, яка передбачає певну форму повторного множення.

5. Повторні операції

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

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

6. Підсумок

Коротше кажучи, фундаментальною причиною помилок в операціях з плаваючою комою є поєднання усікання апаратних засобів і усічення зворотного у випадку поділу. Оскільки стандарт IEEE-754 вимагає лише помилки менше половини однієї одиниці в останньому місці для однієї операції, помилки з плаваючою комою над повторними операціями будуть додаватися, якщо не буде виправлено.


8
(3) неправильно. Похибка округлення в поділі не менше однієї одиниці на останньому місці, але щонайбільше половини одиниці на останньому місці.
gnasher729

6
@ gnasher729 Хороший улов. Більшість основних операцій також мають останню помилку менше ніж 1/2 однієї одиниці, використовуючи режим округлення IEEE за замовчуванням. Відредагував пояснення, а також зазначив, що помилка може бути більшою, ніж 1/2 одного ulp, але менше 1 ulp, якщо користувач перекриває режим округлення за замовчуванням (це особливо стосується вбудованих систем).
KernelPanik

39
(1) з плаваючою комою числа не мають помилок. Кожне значення з плаваючою комою - саме те, що воно є. Більшість (але не всі) операції з плаваючою точкою дають неточні результати. Наприклад, немає бінарного значення з плаваючою комою, яке б точно було рівним 1,0 / 10,0. Деякі операції (наприклад, 1,0 + 1,0) дійсно дають точні результати, з іншого боку.
Соломон повільно

19
"Основна причина помилки при поділі з плаваючою комою - це алгоритми поділу, які використовуються для обчислення коефіцієнта", - це дуже помилково. Для відповідного поділу IEEE-754 єдиною причиною помилок у поділі з плаваючою комою є нездатність результату бути точно представленим у форматі результату; той же результат обчислюється незалежно від використовуваного алгоритму.
Стівен Канон

6
@Matt Вибачте за пізню відповідь. В основному це пов'язано з питаннями ресурсу / часу та компромісами. Існує спосіб зробити довгий поділ / більш «нормальний» поділ, він називається СРТ-дивізіоном з радіусом два. Однак це неодноразово зміщується і віднімає дільник від дивіденду і займає багато тактових циклів, оскільки він обчислює лише один біт коефіцієнта на цикл годин. Ми використовуємо таблиці зворотних записів, щоб ми могли обчислити більше бітів коефіцієнта за цикл і зробити ефективні компроміси щодо продуктивності / швидкості.
KernelPanik

462

Перетворюючи .1 або 1/10 в базу 2 (двійковий), ви отримуєте повторюваний візерунок після десяткової крапки, як і намагаючись представити 1/3 в базі 10. Значення не є точним, і тому ви не можете зробити точна математика з ним, використовуючи звичайні методи з плаваючою точкою.


133
Чудова і коротка відповідь. Повторний зразок виглядає як 0,00011001100110011001100110011001100110011001100110011 ...
Костянтин Чернов

4
Це не пояснює, чому не використовується кращий алгоритм, який не перетворює в бінарні файли в першу чергу.
Дмитро Зайцев

12
Тому що продуктивність. Використання двійкового файлу в кілька тисяч разів швидше, тому що воно власне для машини.
Джоел Куехорн

7
Є методи, які дають точні десяткові значення. BCD (двійковий кодований десятковий) або різні інші форми десяткового числа. Однак вони обидва повільніше (на ЛОТ повільніше) і займають більше місця, ніж використання двійкової плаваючої точки. ( В якості прикладу, упаковані BCD зберігає 2 десяткових цифр в байті Це 100 можливих значень байт в тому , що на самому справі може зберігати 256 можливих значень, або 100/256, який відходів близько 60% можливих значень байта ..)
Duncan C

16
@Jacksonkr ти все ще думаєш у базі-10. Комп'ютери базові-2.
Joel Coehoorn

306

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

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

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

Тепер, як би ви склали всі шматочки таким чином, щоб додати до однієї десятої (0,1) або однієї п’ятої (0,2) піци? Дійсно подумай над цим і спробуй це опрацювати. Ви навіть можете спробувати використовувати справжню піцу, якщо у вас є під рукою міфічна точність різака для піци. :-)


Більшість досвідчених програмістів, звичайно, знають справжню відповідь, яка полягає в тому, що немає можливості зібрати точну десяту або п’яту частину піци за допомогою цих скибочок, незалежно від того, наскільки тонко ви їх нарізаєте. Ви можете зробити досить гарне наближення, і якщо скласти наближення 0,1 з наближенням 0,2, ви отримаєте досить гарне наближення 0,3, але все одно це лише наближення.

Для подвійних точних цифр (це точність, яка дозволяє вдвічі зменшити піцу в 53 рази), цифри одразу менші та більші за 0,1 - 0,099999999999999167332731531132594682276248931884765625 та 0,1000000000000000055511151231257827021181583404541015625. Останнє трохи ближче до 0,1, ніж перше, тому числовий аналізатор буде, даючи вхід 0,1, надавати перевагу останньому.

(Різниця між цими двома числами - це "найменший зріз", який ми маємо вирішити або включити, який вводить ухил вгору, або виключити, що вносить ухил вниз. Технічний термін для цього найменшого шматочка - це ulp .)

У випадку 0,2 цифри все однакові, просто збільшені на коефіцієнт 2. Знову ми віддаємо перевагу значенню, яке трохи вище 0,2.

Зауважте, що в обох випадках наближення для 0,1 та 0,2 мають невеликий зміщення вгору. Якщо ми додамо достатньо цих упереджень, вони відсунуть число все далі і далі від того, що ми хочемо, і насправді, у випадку 0,1 + 0,2, зміщення буде досить високим, що отримане число вже не є найближчим числом до 0,3.

Зокрема, 0,1 + 0,2 - це дійсно 0,1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, тоді, як число цифри є найменшим, а потім число факту становить, що є найменшим числом.


PS Деякі мови програмування також надають різаки для піци, які можуть розділити скибочки на точні десяті частини . Хоча такі різаки для піци є рідкістю, якщо у вас є доступ до однієї, вам слід користуватися нею, коли важливо мати можливість отримати рівно одну десяту або п’яту частину шматочка.

(Спочатку розміщено на Quora.)


3
Зауважте, що є деякі мови, які включають точну математику. Один із прикладів - схема, наприклад, через GNU Guile. Дивіться draketo.de/english/exact-math-to-the-rescue - вони зберігають математику як дроби і в кінці лише розрізають .
Arne Babenhauserheide

5
@FloatingRock Насправді дуже мало основних мов програмування мають вбудовані раціональні числа. Арн - так само, як і я, - це речі, які ми зіпсуємо.
Кріс Єстер-Янг

5
@ArneBabenhauserheide Я думаю, що варто додати, що це буде працювати лише з раціональними числами. Отже, якщо ви займаєтесь математикою з ірраціональними числами, такими як pi, вам доведеться зберігати це як кратне число pi. Звичайно, будь-який обчислення, що включає pi, не може бути представлений у вигляді точного десяткового числа.
Айдіакапі

13
@connexo Гаразд. Як би ви запрограмували ротатор для піци на 36 градусів? Що таке 36 градусів? (Підказка: якщо ви в змозі визначити це точно, у вас також є шматочки-точні десяті різаки для піци.) Іншими словами, ви насправді не можете мати 1/360 (градус) або 1 / 10 (36 градусів) лише з двійковою плаваючою точкою.
Кріс Єстер-Янг

12
@connexo Також "кожен ідіот" не може обертати піцу рівно на 36 градусів. Люди занадто схильні до помилок, щоб робити щось досить точне.
Кріс Єстер-Янг

212

Помилки округлення з плаваючою комою. 0,1 не може бути представлено настільки точно в базі-2, як у базовій-10, через відсутність основного коефіцієнта 5. Так само, як 1/3 має нескінченну кількість цифр для подання у десятковій формі, але "0,1" у базі-3, 0,1 приймає нескінченну кількість цифр у базі-2, де немає у базі-10. А комп'ютери не мають нескінченної кількості пам'яті.


133
комп’ютерам не потрібен нескінченний об'єм пам'яті, щоб отримати 0,1 + 0,2 = 0,3 правильно
Pacerier

23
@Pacerier Звичайно, вони могли використовувати два цілі числа з необмеженою точністю, щоб представити дріб, або вони могли використовувати позначення цитати. Саме конкретне поняття «двійковий» або «десятковий» робить це неможливим - ідея про те, що у вас є послідовність двійкових / десяткових цифр і десь там, точка радіації. Для отримання точних раціональних результатів нам потрібен кращий формат.
Девін Жанп'єр

15
@Pacerier: Ні двійкова, ні десяткова плаваюча точка не може точно зберігати 1/3 або 1/13. Десяткові типи з плаваючою комою можуть точно представляти значення форми M / 10 ^ E, але менш точні, ніж двійкові числа з плаваючою комою аналогічного розміру, якщо мова йде про представлення більшості інших дробів . У багатьох додатках корисніше мати більш високу точність з довільними дробами, ніж мати ідеальну точність з кількома "спеціальними".
supercat

13
@Pacerier Вони роблять, якщо вони зберігають числа як двійкові поплавці, що було суть відповіді.
Марк Амері

3
@chux: Різниця в точності між двійковими та десятковими типами не величезна, але різниця 10: 1 у кращому та найгіршому випадку для десяткових типів набагато більша, ніж різниця 2: 1 у двійкових типах. Мені цікаво, чи хтось створив апаратне чи письмове програмне забезпечення для ефективної роботи будь-якого з десяткових типів, оскільки це не здавалося б придатним до ефективної реалізації апаратних чи програмних засобів.
supercat

121

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

Наприклад:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... замість:

var result = 0.1 + 0.2;     // result === 0.3 returns false

Вираз 0.1 + 0.2 === 0.3повертається falseв JavaScript, але, на щастя, ціла арифметика з плаваючою комою є точною, тому помилки десяткового представлення можна уникнути шляхом масштабування.

Як практичний приклад, щоб уникнути проблем з плаваючою комою, де точність є найважливішою, рекомендується 1 обробляти гроші як ціле число, що представляє кількість центів: 2550центів замість 25.50доларів.


1 Дуглас Крокфорд: JavaScript: Хороші частини : Додаток A - Страшні частини (стор. 105) .


3
Проблема полягає в тому, що саме перетворення є неточним. 16.08 * 100 = 1607.9999999999998. Чи потрібно вдаватися до поділу числа та перетворення окремо (як у 16 ​​* 100 + 08 = 1608)?
Джейсон

38
Рішення тут полягає в тому, щоб зробити всі свої обчислення на цілі числа, потім розділити їх на пропорцію (100 в даному випадку) і округлити лише при поданні даних. Це забезпечить, що ваші розрахунки завжди будуть точними.
Девід Гранадо

15
Тільки, щоб трішки вибирати: ціла арифметика є точною лише з плаваючою точкою до точки (призначена каламбур). Якщо число більше 0x1p53 (для використання шістнадцяткової нотації з плаваючою точкою Java 7, = 9007199254740992), то ulp в цій точці дорівнює 2, і так 0x1p53 + 1 округляється до 0x1p53 (а 0x1p53 + 3 округляється до 0x1p53 + 4, через круглу до рівну). :-D Але, звичайно, якщо ваше число менше 9 квадрильйонів, вам слід добре. :-P
Кріс Єстер-Янг

2
Джейсоне, вам слід просто округлювати результат (int) (16.08 * 100 + 0.5)
Михайло Семенов

@CodyBugstein " Отже, як отримати .1 + .2, щоб показати .3? " Напишіть власну функцію друку, щоб розмістити десяткову точку, де ви хочете.
RonJohn

113

Моя відповідь досить довга, тому я розділив її на три розділи. Оскільки питання стосується математики з плаваючою комою, я наголосив на тому, що насправді робить машина. Я також конкретизував подвійну (64-бітну) точність, але аргумент застосовується однаково до будь-якої арифметики з плаваючою точкою.

Преамбула

IEEE 754 з подвійною точністю в довічним форматі з плаваючою точкою (binary64) число являє собою число виду

значення = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

в 64 бітах:

  • Перший біт - це бітовий знак : 1якщо число від’ємне, 0інакше 1 .
  • Наступні 11 біт - це показник , який компенсується на 1023. Іншими словами, після зчитування бітів експонента з числа з подвоєною точністю 1023 необхідно відняти, щоб отримати потужність двох.
  • Решта 52 біта - це знамення (або мантіса). У мантісі «мається на увазі» 1.завжди 2 опущено, оскільки найзначніший біт будь-якого бінарного значення є 1.

1 - IEEE 754 дозволяє концепцію підписаного нуля - +0і -0трактуються по-різному: 1 / (+0)є позитивна нескінченність; 1 / (-0)є негативною нескінченністю. При нульових значеннях біти мантіси та експонента всі нульові. Примітка: нульові значення (+0 і -0) явно не класифікуються як деннормальні 2 .

2 - Це не стосується деннормальних чисел , які мають показник зміщення нуля (і мається на увазі 0.). Діапазон деннормальних чисел подвійної точності d min ≤ | x | ≤ d max , де d min (найменше представлене ненульове число) 2 -1023 - 51 (≈ 4,94 * 10 -324 ) і d max (найбільше денормальне число, з якого мантіса складається повністю з 1s) становить 2 -1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).


Перетворення подвійного числа точності у двійкове

Існує багато інтернет-перетворювачів для перетворення подвійної точності з плаваючою точкою в двійкову (наприклад, на binaryconvert.com ), але ось деякий зразок коду C #, щоб отримати представлення IEEE 754 для подвійного числа точності (я розділяю три частини двокрапками ( :) :

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

До речі, питання: оригінальне запитання

(Пропустити донизу для версії TL; DR)

Катон Джонстон (запитуючий питання) запитав, чому 0,1 + 0,2! = 0,3.

IEEE 754, що пишеться у двійковій формі (з двокрапками, що розділяють три частини),:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Зауважте, що мантія складається з повторюваних цифр 0011. Це є ключовим для того, чому існує якась помилка обчислень - 0,1, 0,2 та 0,3 не можуть бути представлені у двійковій точності в кінцевій кількості бінарних бітів, більше ніж 1/9, 1/3 або 1/7 можуть бути представлені точно в десяткових цифр .

Також зауважте, що ми можемо зменшити потужність в експоненті на 52 і зрушити точку в бінарному поданні праворуч на 52 місця (приблизно як 10 -3 * 1,23 == 10 -5 * 123). Потім це дозволяє представити двійкове уявлення як точне значення, яке воно представляє у вигляді a * 2 p . де 'a' - ціле число.

Перетворення експонентів у десяткові, видалення зміщення та повторне додавання мається на увазі 1(у квадратних дужках) 0,1 та 0,2:

0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

Щоб додати два числа, показник повинен бути однаковим, тобто:

0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

Оскільки сума не має виду 2 n * 1. {bbb}, збільшуємо показник на один і зміщуємо десяткову ( двійкову ) точку, щоб отримати:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

Зараз у мантісі 53 біти (53-й біт знаходиться у квадратних дужках у рядку вище). Режим округлення за замовчуванням для IEEE 754 - " Круглий до найближчого ", тобто якщо число x падає між двома значеннями a і b , вибирається значення, де найменший значущий біт дорівнює нулю.

a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

Зауважте, що a і b відрізняються лише останнім бітом; ...0011+ 1= ...0100. У цьому випадку значення з найменшим значущим бітом нуля дорівнює b , тому сума дорівнює:

sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

тоді як двійкове представлення 0,3 становить:

0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

що відрізняється лише від двійкового представлення суми 0,1 та 0,2 на 2 -54 .

Двійкове представлення 0,1 і 0,2 є найбільш точним поданням чисел, дозволених IEEE 754. Додавання цього подання, завдяки режиму округлення за замовчуванням, призводить до значення, яке відрізняється лише найменш-значущим бітом.

TL; DR

Запис 0.1 + 0.2у бінарне представлення IEEE 754 (з двокрапками, що розділяють три частини) та порівняння з цим 0.3, це (я помістив окремі біти у квадратні дужки):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Ці значення, перетворені назад у десяткові,:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

Різниця становить рівно 2 -54 , що становить ~ 5,5511151231258 × 10 -17 - незначне (для багатьох застосувань) порівняно з вихідними значеннями.

Порівнювати останні кілька біт числа числа з плаваючою комою за своєю суттю небезпечно, тому що кожен, хто читає відоме " Що повинен знати кожен комп'ютерний вчений про арифметику з плаваючою комою " (який охоплює всі основні частини цієї відповіді).

Більшість калькуляторів використовують додаткові цифри охоронця, щоб подолати цю проблему, і ось як 0.1 + 0.2це дасть 0.3: останні кілька бітів округлюються.


14
Мою відповідь було визнано невдовзі після публікації. З тих пір я вніс багато змін (у тому числі явно відзначаючи повторювані біти при написанні 0,1 та 0,2 у двійковій формі, які я опустив у оригіналі). Ви можете, будь-ласка, дати мені відгук, щоб я міг покращити свою відповідь? Я відчуваю, що моя відповідь додає щось нове, оскільки обробка суми в IEEE 754 в інших відповідях не охоплюється так само. Хоча "Те, що повинен знати кожен вчений-комп'ютер ..." стосується того самого матеріалу, моя відповідь стосується конкретно випадку 0,1 + 0,2.
Вай Ха Лі

57

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

Якби комп’ютер працював у базі 10, 0.1був би 1 x 10⁻¹, 0.2був би 2 x 10⁻¹і 0.3був би 3 x 10⁻¹. Математика з цілим числом проста та точна, тому додавання 0.1 + 0.2очевидно призведе до 0.3.

Комп'ютери зазвичай не працюють в базі 10, вони працюють в базі 2. Ви все одно можете отримати точні результати для деяких значень, наприклад, 0.5є 1 x 2⁻¹і 0.25є 1 x 2⁻², і додаючи їх результати в 3 x 2⁻², або 0.75. Саме так.

Проблема пов'язана з числами, які можна точно представити в базі 10, але не в базі 2. Ці числа потрібно округлити до їх найближчого еквівалента. Якщо припустити , що дуже загальний 64-бітний формат IEEE з плаваючою точкою, найближче число до 0.1є 3602879701896397 x 2⁻⁵⁵, і найближче число до 0.2є 7205759403792794 x 2⁻⁵⁵; додавання їх разом призводить до 10808639105689191 x 2⁻⁵⁵або точне десяткове значення 0.3000000000000000444089209850062616169452667236328125. Номери з плаваючою комою, як правило, округлені для відображення.


2
@Mark Дякую за це чітке пояснення, але тоді виникає питання, чому 0,1 + 0,4 точно дорівнює 0,5 (як мінімум у Python 3). Також який найкращий спосіб перевірити рівність при використанні плавців у Python 3?
пчегор

2
@ user2417881 Операції з плаваючою точкою IEEE мають правила округлення для кожної операції, і іноді округлення може давати точну відповідь, навіть коли два числа відключені трохи. Деталі занадто довгі для коментарів, і я в будь-якому випадку не є експертом з них. Як ви бачите у цій відповіді, 0,5 - це одне з небагатьох десяткових знаків, які можна представити у двійковій формі, але це лише збіг обставин. Для перевірки рівності см stackoverflow.com/questions/5595425 / ... .
Марк Рансом

1
@ user2417881 ваше питання мене заінтригувало, тому я перетворив його на повне запитання та відповідь: stackoverflow.com/q/48374522/5987
Марк Викуп

47

Помилка округлення з плаваючою комою. З того, що повинен знати кожен вчений-комп’ютер про арифметику з плаваючою комою :

Видавлення нескінченно багатьох реальних чисел на кінцеве число біт вимагає приблизного подання. Хоча існує нескінченна кількість цілих чисел, у більшості програм результат цілочисельних обчислень може зберігатися в 32 бітах. На відміну від будь-якої фіксованої кількості біт, більшість обчислень з реальними числами дають величини, які неможливо точно представити за допомогою такої кількості біт. Тому результат обчислення з плаваючою комою часто повинен бути округленим, щоб повернутися до його кінцевого зображення. Ця помилка округлення є характерною особливістю обчислення з плаваючою комою.


33

Моє вирішення:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

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


30

Було розміщено багато хороших відповідей, але я хотів би додати ще одну.

Не всі числа можуть бути представлені через поплавці / подвійні Наприклад, число "0,2" буде представлено як "0.200000003" в одній точності в стандарті IEEE754 з плаваючою точкою.

Моделі для зберігання реальних цифр під капотом представляють плаваючі числа як

введіть тут опис зображення

Незважаючи на те, що ви можете 0.2легко набрати , FLT_RADIXі DBL_RADIXце 2; не 10 для комп'ютера з FPU, який використовує "Стандарт IEEE для двійкової арифметики з плаваючою точкою (ISO / IEEE Std 754-1985)".

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


28

Деякі статистичні дані, пов'язані з цим відомим питанням подвійної точності.

При додаванні всіх значень ( a + b ) за допомогою кроку 0,1 (від 0,1 до 100) ми маємо ~ 15% шансів помилки точності . Зауважте, що помилка може призвести до дещо більших або менших значень. Ось кілька прикладів:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

При відніманні всіх значень ( a - b, де a> b ) з використанням кроку 0,1 (від 100 до 0,1), у нас ~ 34% шансів помилки точності . Ось кілька прикладів:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* 15% і 34% справді величезні, тому завжди використовуйте BigDecimal, коли точність має велике значення. З двома десятковими цифрами (крок 0.01) ситуація погіршується дещо більше (18% та 36%).


28

Ні, не розбитий, але більшість десяткових дробів повинні бути приблизні

Підсумок

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

Навіть прості числа, такі як 0,01, 0,02, 0,03, 0,04 ... 0,24, не представлені точно як двійкові дроби. Якщо ви порахуєте 0,01, .02, .03 ..., не досягнете рівня 0,25, ви отримаєте перший дріб, представлений у базі 2 . Якби ви спробували це, використовуючи FP, ваш 0,01 був би трохи відключений, тому єдиний спосіб додати 25 з них до приємного точного 0,25 зажадав би довгий ланцюжок причинності, що включає захисні біти та округлення. Важко передбачити, тому ми кидаємо руки і кажемо: "FP - це не точно", але це не зовсім так.

Ми постійно надаємо апаратному забезпеченню FP те, що здається простим у базі 10, але є повторюваним дробом у базі 2.

Як це сталося?

Коли ми пишемо у десятковій формі , кожен дріб (конкретно, кожен закінчуючий десятковий) є раціональним числом форми

           a / (2 n x 5 м )

У двійковій формі ми отримуємо лише 2 n додаток, тобто:

           а / 2 н

Таким чином , в десяткової системі , ми не можемо уявити 1 / 3 . Оскільки база 10 включає 2 в якості основного множника, кожне число, яке ми можемо записати як двійковий дріб, також може бути записане як дріб 10. Однак навряд чи що-небудь, що ми пишемо як базовий 10 дріб, є репрезентативним у двійковій формі . У діапазоні від 0,01, 0,02, 0,03 ... 0,99 у нашому форматі FP можуть бути представлені лише три числа: 0,25, 0,50 та 0,75, оскільки вони є 1/4, 1/2 та 3/4, усі числа з простим фактором, використовуючи лише 2 n доданок.

У базі 10 ми не можемо уявити 1 / 3 . Але в двійковому коді, ми не можемо зробити +1 / +10 або +1 / +3 .

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

Справа з цим

Зазвичай розробникам доручають робити <epsilon зіставлення, кращою порадою може бути округлення до цілісних значень (у бібліотеці C: round () та roundf (), тобто перебування у форматі FP), а потім порівняння. Округлення до певної десяткової довжини дробу вирішує більшість проблем з виходом.

Крім того, про реальні проблеми зі скороченням чисельності (проблеми, які FP був винайдений на ранніх, страшенно дорогих комп'ютерах) фізичні константи Всесвіту та всі інші вимірювання відомі лише відносно невеликій кількості значущих цифр, тому весь проблемний простір так чи інакше був "неточним". "Точність" FP не є проблемою в цьому застосуванні.

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

Мені подобається відповідь Піци від Кріса , тому що вона описує актуальну проблему, а не лише звичайне рукоділля про "неточність". Якби FP були просто "неточними", ми могли б це виправити і зробили б це десятиліття тому. Причиною у нас є те, що формат FP компактний і швидкий, і це найкращий спосіб розчавити велику кількість цифр. Крім того, це спадщина від космічної епохи та гонки озброєнь та ранніх спроб вирішити великі проблеми з дуже повільними комп'ютерами за допомогою невеликих систем пам'яті. (Інколи окремі магнітні ядра для 1-бітового зберігання, але це вже інша історія. )

Висновок

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


Округлення до найближчого цілого числа не є безпечним способом вирішення проблеми порівняння у всіх випадках. 0,4999998 і 0,500001 округлюються до різних цілих чисел, тому навколо кожної точки відсікання існує "небезпечна зона". (Я знаю, що ці десяткові рядки, ймовірно, не є точно представними як двійкові поплавці IEEE.)
Пітер Кордес

1
Крім того, незважаючи на те, що плаваюча точка є форматом "спадщина", він дуже добре розроблений. Я не знаю нічого, що хтось змінив би, якщо переробляти його зараз. Чим більше я дізнаюся про це, тим більше я думаю, що це дійсно добре розроблено. наприклад, зміщений показник означає, що послідовні двійкові поплавці мають послідовне подання цілих чисел, тому ви можете реалізувати nextafter()з цілим збільшенням або зменшенням бінарного представлення поплавця IEEE. Крім того, ви можете порівнювати поплавці як цілі числа і отримати правильну відповідь, за винятком випадків, коли вони обидва негативні (через величину знака та доповнення 2).
Пітер Кордес

Я не погоджуюся, поплавці повинні зберігатися як десяткові знаки, а не двійкові, і всі проблеми вирішені.
Ронен Фестінгер

Чи " x / (2 ^ n + 5 ^ n) " не повинен бути " x / (2 ^ n * 5 ^ n) "?
Вай Ха Лі

@RonenFestinger - а як щодо 1/3?
Стівен C

19

Ви пробували рішення з клейкої стрічки?

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

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

У мене була така ж проблема в науковому симуляційному проекті в #


19

Щоб запропонувати найкраще рішення, я можу сказати, що я виявив наступний метод:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Дозвольте пояснити, чому це найкраще рішення. Як відповіли інші, зазначені вище, корисно використовувати функцію Javascript toFixed () для вирішення проблеми. Але, швидше за все, ви зіткнетеся з деякими проблемами.

Уявіть , що ви збираєтеся скласти два числа з плаваючою точкою , як 0.2і 0.7тут: 0.2 + 0.7 = 0.8999999999999999.

Очікуваний результат - 0.9це означає, що вам потрібен результат з однозначною точністю в цьому випадку. Отже, ви повинні були використати(0.2 + 0.7).tofixed(1) але ви не можете просто дати певний параметр toFixed (), оскільки це залежить від даного числа, наприклад

`0.22 + 0.7 = 0.9199999999999999`

У цьому прикладі вам потрібна двоцифрова точність, так і має бути toFixed(2), тож яким повинен бути параметр для розміщення кожного заданого числа з плавкою?

Ви можете сказати, нехай це буде 10 у будь-якій ситуації тоді:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

Чорт! Що ти будеш робити з тими небажаними нулями після 9? Настав час перетворити його на плаваючий, щоб зробити це так, як вам захочеться:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Тепер, коли ви знайшли рішення, краще запропонувати його як таку функцію:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Спробуємо самі:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Ви можете використовувати його таким чином:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Як W3SCHOOLS передбачає, що є ще одне рішення, ви можете помножити та розділити, щоб вирішити проблему вище:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

Майте на увазі, що (0.2 + 0.1) * 10 / 10це не буде працювати взагалі, хоча це здається таким же! Я вважаю за краще перше рішення, оскільки можу застосувати його як функцію, яка перетворює вхідний поплавок у точний вихідний поплавок.


це зробило мені справжній головний біль. Я сумую 12 плаваючих чисел, потім показую суму і середнє значення, якщо ці числа. за допомогою toFixed () може виправити підсумовування двох чисел, але при підсумовуванні кількох чисел стрибок є значним.
Нурягди Мустапаєв

@Nuryagdy Mustapayev Я не отримав вашого наміру, тому що я перевірив, перш ніж ви зможете підсумувати 12 плаваючих чисел, а потім скористайтеся функцією floatify () за результатами, потім виконайте все, що завгодно на ньому, я не спостерігав жодних проблем з його використанням.
Мохаммед Мусаві

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

16

Ці дивні числа з’являються тому, що комп’ютери використовують двійкову (базова 2) система числення для обчислення, тоді як ми використовуємо десяткову (базу 10).

Є більшість дробових чисел, які не можуть бути представлені точно ні в двійкових, ні в десяткових або в обох випадках. Результат - результати округлого (але точного) числа.


Я взагалі не розумію вашого другого абзацу.
Ней

1
@Nae Я би переклав другий абзац як "Більшість дробів не може бути представлено точно ні в десятковій, ні у двійковій формі . Тому більшість результатів буде закруглено - хоча вони все одно будуть точні до кількості бітів / цифр, притаманних представленню використовується ".
Стів Саміт

15

Багато численних дублікатів цього питання запитують про вплив округлення плаваючої точки на конкретні числа. На практиці легше зрозуміти, як це працює, дивлячись на точні результати обчислень, що цікавлять, а не просто читати про це. Деякі мови пропонують способи цього зробити, наприклад, перетворення на floatабо doubleвBigDecimal в Java.

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

Застосовуючи його до цифр у питанні, трактуються як подвійні:

0,1 перетворює на 0,1000000000000000055511151231257827021181583404541015625,

0,2 перетворює на 0.200000000000000011102230246251565404236316680908203125,

0,3 перетворює на 0,299999999999999988897769753748434595763683319091796875 та

0,30000000000000004 перетворюється на 0,3000000000000000444089209850062616169452667236328125.

Додавання перших двох чисел вручну або в десятковий калькулятор, такий як калькулятор повної точності , показує точну суму фактичних входів 0,3000000000000000166533453693773481063544750213623046875.

Якби її округлили до еквівалента 0,3, помилка округлення склала б 0,0000000000000000277555756156289135105907917022705078125. Округлення до еквівалента 0,30000000000000004 також дає помилку округлення 0,0000000000000000277555756156289135105907917022705078125. Застосовується вимикач з круглим до рівним рівнем.

Повертаючись до перетворювача з плаваючою комою, необмеженою шістнадцяткою для 0,30000000000000004 є 3fd3333333333334, яка закінчується парною цифрою і тому є правильним результатом.


2
Особі, редагування якої я щойно відкотив: я вважаю кодові котирування підходящими для кодування коду. Ця відповідь, будучи нейтральною до мови, зовсім не містить кодованого коду. Числа можна використовувати в англійських реченнях, і це не перетворює їх на код.
Патрісія Шанахан

Це , ймовірно , чому хтось - то відформатований ваші номери в вигляді коду - не для форматування, але для зручності читання.
Вай Ха Лі

... також, кругле число навіть відноситься до двійкового подання, а не до десяткового подання. Дивіться це або, наприклад, це .
Вай Ха Лі

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

14

З огляду на те, що про це ніхто не згадував ...

Деякі мови високого рівня, такі як Python та Java, оснащені інструментами для подолання обмежень з бінарними плаваючими точками. Наприклад:

  • decimalМодуль Python та BigDecimalклас Java , які представляють числа внутрішньо з десятковою нотацією (на відміну від двійкової нотації). Обидва мають обмежену точність, тому вони все ще схильні до помилок, однак вони вирішують найпоширеніші проблеми з арифметикою з плаваючою комою з двійковим кодом.

    Десяткові знаки дуже приємні при роботі з грошима: десять центів плюс двадцять копійок завжди рівно тридцять центів:

    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True
    

    decimalМодуль Python базується на стандарті IEEE 854-1987 .

  • fractionsМодуль Python та BigFractionклас Apache Common . Обидва представляють раціональні числа як (numerator, denominator)пари, і вони можуть дати більш точні результати, ніж арифметична десяткова плаваюча точка.

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


14

Чи можу я просто додати; люди завжди вважають, що це проблема комп’ютера, але якщо ви порахуєте своїми руками (основа 10), ви не можете отримати, (1/3+1/3=2/3)=trueякщо не маєте нескінченності, щоб додати 0,333 ... до 0,333 ... так само, як і(1/10+2/10)!==3/10 проблеми в базі 2, ви усікаєте його до 0,333 + 0,333 = 0,666 і, ймовірно, округлите його до 0,667, що також буде технічно неточним.

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


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

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

Бінарну арифметику @RonenFestinger легко здійснити на комп’ютерах, оскільки для неї потрібно лише вісім основних операцій з цифрами: скажімо $ a $, $ b $ в $ 0,1 $, що вам потрібно знати, це $ \ operatorname {xor} (a, b) $ і $ \ operatorname {cb} (a, b) $, де xor є ексклюзивним або, а cb - "біт переносу", який становить $ 0 $ у всіх випадках, за винятком випадків, коли $ a = 1 = b $, і в цьому випадку ми маємо один (адже комунікативність усіх операцій економить ваші випадки в $ 2 $, і все, що вам потрібно, - це $ 6 $ правил). Для десяткового розширення потрібно зберігати випадки $ 10 \ раз 11 $ (у десятковій нотації) та різні $ 10 $ стани для кожного біта та сховища відходів на носі.
Оскар Лімка

@RonenFestinger - Десяткові числа НЕ точніші. Ось що говорить ця відповідь. Для будь-якої обраної вами бази існуватимуть раціональні числа (дроби), які дають нескінченно повторювані послідовності цифр. Для запису, деякі з перших комп'ютерів були використовувати базу 10 подань для чисел, але комп'ютерна техніка дизайнерів піонерських незабаром прийшли до висновку , що база 2 було набагато простіше і ефективніше реалізувати.
Стівен C

9

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

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

Якщо вам потрібна нескінченна точність (наприклад, використовуючи число π, замість однієї з багатьох його коротших опор), слід написати або використовувати символічну математичну програму.

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


9

Для задоволення я грав із поданням поплавків, дотримуючись визначень із стандарту C99 і написав код нижче.

Код друкує двійкове представлення поплавків у 3 відокремлених групах

SIGN EXPONENT FRACTION

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

Отже, коли ви пишете float x = 999..., компілятор перетворить це число у бітове представлення, надруковане функцією, xxтаким чином, що сума, надрукована функцією, yyбуде дорівнює заданому номеру.

Насправді ця сума є лише наближенням. Для числа 999,999,999 компілятор вставить у бітове представлення поплавця число 1 000 000 000

Після коду я додаю консольний сеанс, в якому я обчислюю суму термінів для обох констант (мінус PI та 999999999), які дійсно існують у апаратному забезпеченні, вставленому туди компілятором.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Ось консольний сеанс, в якому я обчислюю реальне значення float, яке існує в апаратному забезпеченні. Я використовував bcдля друку суму термінів, виведених основною програмою. Можна також вставити цю суму в пітон replабо щось подібне.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Це воно. Значення 999999999 насправді

999999999.999999446351872

Ви також можете перевірити, bcщо -3.14 також обурений. Не забудьте встановити scaleфактор bc.

Відображена сума - це те, що знаходиться всередині обладнання. Значення, яке ви отримуєте, обчислюючи його, залежить від встановленої вами шкали. Я встановив scaleкоефіцієнт на 15. Математично, з нескінченною точністю, здається, це 1 000 000 000.


5

Ще один спосіб переглянути це: використовуються 64 біти для представлення чисел. Як наслідок, не можна точно представити більше 2 ** 64 = 18,446,744,073,709,551,616 різних чисел.

Однак Math каже, що вже існує нескінченно багато десяткових знаків між 0 і 1. IEE 754 визначає кодування для ефективного використання цих 64 біт для набагато більшого простору чисел плюс NaN та +/- Нескінченність, тому між чітко представленими числами заповнюються прогалини числа лише приблизні.

На жаль, 0,3 сидить у розриві.


4

Уявіть, що ви працюєте в базовій частині десяти, скажімо, з 8 цифр точності. Ви перевіряєте, чи

1/3 + 2 / 3 == 1

і дізнайся, що це повертається false . Чому? Ну, як справжні цифри у нас

1/3 = 0,333 .... і 2/3 = 0,666 ....

Обрізаючи вісім знаків після коми, ми отримуємо

0.33333333 + 0.66666666 = 0.99999999

що, звичайно, відрізняється від 1.00000000точно 0.00000001.


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

1/10 = 0,0001100110011001100 ... (база 2)

і

1/5 = 0,0011001100110011001 ... (база 2)

Якби ми усіли їх до, скажімо, семи біт, тоді ми отримаємо

0.0001100 + 0.0011001 = 0.0100101

а з іншого боку,

3/10 = 0,01001100110011 ... (база 2)

який, усічений до семи біт, є 0.0100110, і вони різняться точно 0.0000001.


Точна ситуація дещо тонкіша, тому що ці цифри, як правило, зберігаються в наукових позначеннях. Так, наприклад, замість того, щоб зберігати 1/10, оскільки 0.0001100ми можемо зберігати його як щось подібне1.10011 * 2^-4 , залежно від того, скільки бітів ми виділили для експонента та мантіси. Це впливає на те, скільки цифр точності ви отримаєте для своїх розрахунків.

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


4

З Python 3.5 ви можете використовувати math.isclose()функцію для тестування приблизної рівності:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False

3

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

Погляньте, наприклад, на https://posithub.org/ , де показано тип номера, який називається posit (та його попередник unum), який обіцяє забезпечити кращу точність з меншою кількістю біт. Якщо моє розуміння є правильним, воно також фіксує тип проблем у питанні. Досить цікавий проект, людина за ним - це математик, доктор Джон Густафсон . Вся справа з відкритим кодом, з багатьма актуальними реалізаціями в C / C ++, Python, Julia та C # ( https://hastlayer.com/arithmetics ).


3

Насправді це досить просто. Коли у вас є система 10 базової (як наша), вона може виражати лише дроби, які використовують простий коефіцієнт бази. Прості коефіцієнти 10 - це 2 та 5. Отже, 1/2, 1/4, 1/5, 1/8 та 1/10 можна виразити чисто, оскільки всі знаменники використовують прості множники 10. На відміну від 1 / 3, 1/6 та 1/7 - всі десяткові знаки, що повторюються, тому що їх знаменники використовують простий коефіцієнт 3 або 7. У двійкових (або базових 2) єдиний простий коефіцієнт - 2. Отже, ви можете чітко виражати дроби, які містять лише 2 як основний фактор. У двійковій частині 1/2, 1/4, 1/8 виражаються чисто як десяткові знаки. Тоді як 1/5 або 1/10 повторюються десятковими знаками. Тож 0,1 та 0,2 (1/10 та 1/5), очищаючи десятичні знаки в системі базової 10, повторюють десяткові числа в системі базової 2, в якій працює комп'ютер. Коли ви займаєтеся математикою цих повторюваних десяткових знаків,

З https://0.30000000000000004.com/


3

Десяткові числа, такі як 0.1, 0.2і 0.3не представлені точно у двокадрових кодованих типах з плаваючою комою. Сума наближень для 0.1та на 0.2відміну від апроксимації, що використовується для цього 0.3, отже, хибність цього 0.1 + 0.2 == 0.3можна зрозуміти тут:

#include <stdio.h>

int main() {
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;
}

Вихід:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

Щоб ці обчислення були оцінені більш надійно, вам потрібно буде використовувати представлення на основі десяткових значень для значень з плаваючою комою. Стандарт C не визначає такі типи за замовчуванням, але як розширення, описане в технічному звіті .

В _Decimal32, _Decimal64і _Decimal128типи можуть бути доступні у вашій системі (наприклад, GCC підтримує їх на обрані цілі , але Clang не підтримує їх на OS X ).


1

Math.sum (javascript) .... вид заміни оператора

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001

Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

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

Math.sum автоматично визначає точність використання

Math.sum приймає будь-яку кількість аргументів


1
Я не впевнений, що ти відповів на питання: " Чому трапляються ці неточності? "
Вай Ха Лі

певним чином ти маєш рацію, але я прийшов сюди через дивну поведінку javascript щодо цього питання ... я просто хочу поділитися типом рішення
bortunac

0

Я щойно бачив це цікаве питання навколо плаваючих точок:

Розглянемо наступні результати:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

Ми можемо ясно бачити точку зупину , коли 2**53+1- все працює відмінно до тих пір 2**53.

>>> (2**53) - int(float(2**53))
0

Введіть тут опис зображення

Це відбувається через подвійну точність двійкового: IEEE 754 подвійної точності бінарного формату з плаваючою комою: binary64

На сторінці Вікіпедії для формату з плаваючою комою з двома точністю :

Двійкова точна двійкова плаваюча точка - це широко використовуваний формат на ПК, завдяки ширшому діапазону порівняно з одноточною плаваючою точкою, незважаючи на її продуктивність та вартість пропускної здатності. Як і у форматі з плаваючою комою з одноточною точністю, йому не вистачає точності на цілі числа, якщо порівнювати з цілим форматом одного розміру. Зазвичай він відомий просто як подвійний. Стандарт IEEE 754 визначає, що бінарний64 має такий:

  • Біт знаку: 1 біт
  • Експонент: 11 біт
  • Значна точність: 53 біта (52 явно зберігаються)

Введіть тут опис зображення

Реальне значення, припущене заданою 64-бітовою датою подвійної точності з заданим зміщеним показником і 52-бітовою часткою, становить

Введіть тут опис зображення

або

Введіть тут опис зображення

Дякую @a_guest, що вказав на мене.


-1

Інше питання було названо як дублікат цього:

Чому в C ++ чому результат cout << xвідрізняється від значення, яке показує налагоджувач x?

У xпитанні є floatзмінною.

Одним із прикладів може бути

float x = 9.9F;

Відладчик показує 9.89999962, вихід coutоперації є 9.9.

Відповідь виявляється, що coutза замовчуванням точність floatдорівнює 6, тому вона округляє до 6 знаків після коми.

Дивіться тут для довідки


1
ІМО - розміщення цього питання було неправильним підходом. Я знаю, що це засмучує, але люди, яким потрібна відповідь на оригінальне запитання (мабуть, тепер видалено!), Тут не знайдуть його. Якщо ви дійсно відчуваєте, що ваша робота заслуговує на збереження, я б запропонував: 1) шукати ще одне питання, на яке це насправді відповідає; 2) створити запитання, на яке було відповідати.
Стівен C
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.