Чи можна отримати 0, віднявши два неоднакових числа з плаваючою точкою?


131

Чи можна отримати поділ на 0 (або нескінченність) у наступному прикладі?

public double calculation(double a, double b)
{
     if (a == b)
     {
         return 0;
     }
     else
     {
         return 2 / (a - b);
     }
}

У звичайних випадках це, звичайно, не буде. Але що, якщо aі bдуже близькі, може (a-b)призвести 0до точності обчислення?

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


49
Мені доведеться спробувати всі комбінації парних пар, це займе певний час :)
Thirler

3
@Thirler звучить як час використовувати для мене тестування JUnit!
Метт Кларк

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

4
@Thirler, певно. "ти не можеш гарантувати, що число, яке ти присвоюєш поплавковій чи подвійній, є точним"
guness

4
Зауважте лише, що повернення 0 у такому випадку може призвести до важкої невдалості налагодження, тому переконайтеся, що ви дійсно хочете повернути 0 замість того, щоб викидати виняток чи повертати NaN.
m0skit0

Відповіді:


132

У Java a - bніколи не дорівнює 0if a != b. Це відбувається тому, що Java мандатує операції з плаваючою точкою IEEE 754, які підтримують денормалізовані числа. З специфікації :

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

Якщо ФПУ працює з денормалізованими числами , віднімання нерівних чисел ніколи не може призвести до нуля (на відміну від множення), також дивіться це питання .

Для інших мов це залежить. Наприклад, в C або C ++, наприклад, підтримка IEEE 754 не є обов'язковою.

Тим НЕ менше, це можливо для вираження 2 / (a - b)до переповнення, наприклад , з a = 5e-308і b = 4e-308.


4
Однак ОП хоче знати про 2 / (ab). Чи можна це гарантувати скінченно?
Taemyr

Дякую за відповідь, я додав посилання на wikipedia для пояснення денормалізованих чисел.
Тирлер

3
@Taemyr Дивіться мою редакцію. Поділ насправді може переповнитися.
nwellnhof

@Taemyr (a,b) = (3,1)=> 2/(a-b) = 2/(3-1) = 2/2 = 1Чи правда це з плаваючою точкою IEEE, я не знаю
Коул Джонсон

1
@ DrewDormann IEEE 754 також необов'язковий для C99. Див. Додаток F до стандарту.
nwellnhof

50

Як вирішення, що з наступного?

public double calculation(double a, double b) {
     double c = a - b;
     if (c == 0)
     {
         return 0;
     }
     else
     {
         return 2 / c;
     }
}

Таким чином, ви не залежите від підтримки IEEE будь-якою мовою.


6
Уникайте проблеми і спростіть тест відразу. Мені подобається.
Джошуа

11
-1 Якщо a=bви не повинні повертатися 0. Поділ на 0IEEE 754 дає вам нескінченність, не виняток. Ви уникаєте проблеми, тому повернення 0- це помилка, яка чекає, що трапиться. Розглянемо 1/x + 1. Якщо x=0це призведе до 1не правильного значення: нескінченність.
Коул Джонсон

5
@ColeJohnson правильна відповідь - це не нескінченність (якщо ви не вказали, з якої сторони походить межа, права сторона = + inf, ліва сторона = -inf, не визначено = невизначено або NaN).
Нік Т

12
@ChrisHayes: Це правильна відповідь на питання про визнання того, що це питання може бути проблемою XY: meta.stackexchange.com/questions/66377/what-is-the-xy-problem
slebetman

17
@ColeJohnson Повернення 0насправді не є проблемою. Це те, що робить ОП у питанні. Ви можете поставити виняток або все, що підходить для ситуації в тій частині блоку. Якщо ви не любите повертатися 0, це має бути критикою питання. Безумовно, те, що ОП не робило жодної позиції на відповідь. Це питання не має нічого спільного з подальшими обчисленнями після завершення даної функції. Наскільки ви знаєте, вимоги програми потребують повернення 0.
jpmc26

25

Ви не отримаєте ділення на нуль незалежно від значення a - b, оскільки поділ з плаваючою комою на 0 не кидає винятку. Це повертає нескінченність.

Тепер єдиний спосіб a == bповернути істину - це якщо aі bмістити такі самі біти. Якщо вони відрізняються лише найменшим значущим бітом, різниця між ними не буде 0.

Редагувати:

Як Віршева коментує правильно, є деякі винятки:

  1. "Не число порівнює" false з самим собою, але матиме однакові бітові шаблони.

  2. -0.0 визначено для порівняння істинних з +0.0, і їх бітові структури відрізняються.

Так що, якщо обидва aі bє Double.NaN, ви досягнете ще пункт, але оскільки NaN - NaNтакож повертається NaN, чи не буде поділу на нуль.


11
Еран; не суворо правда. "Не число порівнює" false з самим собою, але матиме однакові бітові шаблони. Також -0.0 визначено для порівняння істинних з +0.0, і їх бітові структури відрізняються.
Вірсавія

1
@Bathsheba я не розглядав ці особливі випадки. Дякуємо за коментар
Еран

2
@Eran, дуже добре, що поділ на 0 поверне нескінченність у плаваючу точку. Додав це до питання.
Тирлер

2
@Prashant, але поділ у цьому випадку не відбудеться, оскільки a == b повернеться істинним.
Еран

3
Насправді ви можете отримати виняток FP для поділу на нуль, це варіант, визначений стандартом IEEE-754, хоча це, мабуть, не те, що більшість людей означатиме з "винятком";)
Voo

17

Тут немає жодного випадку, коли тут може відбутися поділ на нуль.

SMT Solver Z3 підтримує точну IEEE арифметику з плаваючою точкою. Попросимо Z3 знайти числа aі bтаке a != b && (a - b) == 0:

(set-info :status unknown)
(set-logic QF_FP)
(declare-fun b () (FloatingPoint 8 24))
(declare-fun a () (FloatingPoint 8 24))
(declare-fun rm () RoundingMode)
(assert
(and (not (fp.eq a b)) (fp.eq (fp.sub rm a b) +zero) true))
(check-sat)

Результат - UNSAT. Таких номерів немає.

Вищевказана рядок SMTLIB також дозволяє Z3 вибрати режим довільного округлення ( rm). Це означає, що результат справедливий для всіх можливих режимів округлення (їх п'ять). Результат також включає можливість, що будь-яка з змінних у грі може бути NaNабо нескінченною.

a == bреалізуються як fp.eqякість , так що +0fі -0fпорівнювати одно. Порівняння з нулем реалізується і з використанням fp.eq. Оскільки питання спрямоване на те, щоб уникнути поділу на нуль, це відповідне порівняння.

Якби тест на рівність здійснювався за допомогою побітової рівності, +0fі -0fце був би спосіб зробити a - bнуль. Невірна попередня версія цієї відповіді містить детальну інформацію про цей випадок для допитливих.

Z3 Online ще не підтримує теорію FPA. Цей результат був отриманий за допомогою останньої нестабільної галузі. Його можна відтворити, використовуючи прив'язки .NET так:

var fpSort = context.MkFPSort32();
var aExpr = (FPExpr)context.MkConst("a", fpSort);
var bExpr = (FPExpr)context.MkConst("b", fpSort);
var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort());
var fpZero = context.MkFP(0f, fpSort);
var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr);
var constraintExpr = context.MkAnd(
        context.MkNot(context.MkFPEq(aExpr, bExpr)),
        context.MkFPEq(subExpr, fpZero),
        context.MkTrue()
    );

var smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

Використання Z3 , щоб відповісти на питання IEEE з плаваючою точкою перетину , тому що це важко ігнорувати випадки (наприклад NaN, -0f, +-inf) , і ви можете задати довільні питання. Не потрібно тлумачити та цитувати технічні характеристики. Ви навіть можете задавати змішані питання з плаваючою чи цілою чисельністю, наприклад " int log2(float)правильний цей алгоритм?"


Чи можете ви додати посилання на SMT Solver Z3 та посилання на онлайн-перекладача? Хоча ця відповідь здається абсолютно законною, хтось може подумати, що ці результати неправильні.
AL

12

Поставлена ​​функція дійсно може повернути нескінченність:

public class Test {
    public static double calculation(double a, double b)
    {
         if (a == b)
         {
             return 0;
         }
         else
         {
             return 2 / (a - b);
         }
    }    

    /**
     * @param args
     */
    public static void main(String[] args) {
        double d1 = Double.MIN_VALUE;
        double d2 = 2.0 * Double.MIN_VALUE;
        System.out.println("Result: " + calculation(d1, d2)); 
    }
}

Вихід є Result: -Infinity.

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


6

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

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

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


6

За старих часів до IEEE 754 цілком можливо, що a! = B не означає ab! = 0 і навпаки. Це було однією з причин створення IEEE 754 в першу чергу.

З IEEE 754 це майже гарантовано. Компіляторам C або C ++ дозволяється робити операцію з більшою точністю, ніж потрібно. Отже, якщо a і b - не змінні, а вирази, то (a + b)! = C не означає (a + b) - c! = 0, тому що a + b можна обчислити один раз з більшою точністю, і один раз без більш висока точність.

Багато FPU можуть бути переведені в режим, коли вони не повертають денормалізовані числа, але замінюють їх 0. У такому режимі, якщо a і b - це крихітні нормовані числа, де різниця менша за найменше нормоване число, але більша за 0, a ! = b також не гарантує a == b.

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


2

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

Номери з плаваючою комою зберігаються більш-менш у наукових позначеннях - тобто, замість 35.2, число, яке зберігається, було б більше схожим на 3,52е2.

Для зручності уявіть, що у нас є одиниця з плаваючою комою, яка працює в базі 10 і має 3 цифри точності. Що відбувається, коли ви віднімаєте 9,99 від 10,0?

1.00e2-9.99e1

Shift, щоб дати кожному значенню однаковий показник

1.00e2-0.999e2

Округніть до 3 цифр

1.00e2-1.00e2

Ой-ой!

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


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

1
"Чи може це статися в кінцевому рахунку, залежить від дизайну FPU" Ні, це не може статися, оскільки визначення Java говорить, що це не може. Дизайн ФПУ не має нічого спільного.
Паскаль Куок

@PascalCuoq: Виправте мене, якщо я помиляюся, але strictfpне ввімкнено, для розрахунків можна отримати значення, які занадто малі, doubleале знаходяться у значенні з плаваючою точкою з розширеною точністю.
supercat

@supercat Відсутність strictfpлише впливає на значення "проміжних результатів", і я цитую з docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4 . aі bє doubleзмінними, а не проміжними результатами, тому їх значення є подвійною точністю, тому кратні 2 ^ -1074. Отже, віднімання цих двох значень подвійної точності кратне 2 ^ -1074, тому ширший діапазон експонентів змінює властивість, що різниця дорівнює 0 iff a == b.
Паскаль Куок

@supercat Це має сенс - для цього вам знадобиться лише один додатковий біт.
Keldor314

1

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

Щоб порівнювати поплавці для рівності, потрібно перевірити, чи значення "достатньо близько" до того самого значення:

if ((first >= second - error) || (first <= second + error)

6
"Ніколи не повинен" трохи сильний, але загалом це хороша порада.
Марк Паттісон

1
Поки ти правдивий, abs(first - second) < error(або <= error) простіше і стисліше.
glglgl

3
Хоча це правда в більшості випадків ( не у всіх ), насправді не відповідає на питання.
тисячоліття

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

1
Якщо ви сортуєте масив за ключем з плаваючою комою, я можу гарантувати, що ваш код не запрацює, якщо ви спробуєте використовувати підказки, порівнюючи числа з плаваючою комою з епсилоном. Тому що гарантія того, що a == b і b == c означає a == c, вже немає. Для хеш-таблиць точно така ж проблема. Коли рівність не є перехідною, ваші алгоритми просто порушуються.
gnasher729

1

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

Не впевнений, що це C ++ або Java, оскільки немає мовного тегу.

double calculation(double a, double b)
{
     if (a == b)
     {
         return nan(""); // C++

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}

1

Основна проблема полягає в тому, що комп’ютерне представлення подвійного (ака-плаваючого чи реального числа в математичній мові) невірно, коли у вас "занадто багато" десятків, наприклад, коли ви маєте справу з подвійним, яке не може бути записане як числове значення ( pi або результат 1/3).

Отже, a == b не може бути виконано з жодним подвійним значенням a і b, як вам поводитися з a == b, коли a = 0,333 і b = 1/3? Залежно від вашої ОС проти FPU порівняно з кількістю мов проти рахунку 3 після 0, у вас буде правда чи помилка.

У будь-якому випадку, якщо ви робите "обчислення подвійного значення" на комп'ютері, вам доведеться мати точність, тому замість того, щоб робити a==b, ви повинні робити absolute_value(a-b)<epsilon, і epsilon є відносно того, що ви моделювали на той час у своєму алгоритмі. Ви не можете мати значення epsilon для всіх ваших подвійних порівнянь.

Коротше кажучи, коли ви вводите a == b, у вас є математичний вираз, який не можна перекласти на комп'ютер (для будь-якого номера з плаваючою комою).

PS: гул, все, на що я тут відповідаю, є ще більш-менш у відповідях та коментарях інших.


1

На основі відгуку @malarres та коментаря @Taemyr, ось мій невеликий внесок:

public double calculation(double a, double b)
{
     double c = 2 / (a - b);

     // Should not have a big cost.
     if (isnan(c) || isinf(c))
     {
         return 0; // A 'whatever' value.
     }
     else
     {
         return c;
     }
}

Моя думка полягає в тому, щоб сказати: найпростіший спосіб дізнатись, чи є результат поділу nan чи inf - це дійсно для виконання поділу.

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