Защо е 8099.99975f != 8100f

Редактиране: Знам, че аритметиката с плаваща запетая не е точна. И аритметиката дори не е мой проблем. Добавката дава резултата, който очаквах. 8099.99975f не го прави.


Така че имам тази малка програма:

public class Test {
    public static void main(String[] args) {
        System.out.println(8099.99975f); // 8099.9995
        System.out.println(8099.9995f + 0.00025f); // 8100.0
        System.out.println(8100f == 8099.99975f); // false
        System.out.println(8099.9995f + 0.00025f == 8099.99975f); // false
        // I know comparing floats with == can be troublesome
        // but here they really should be equal in every bit.
    }
}

Написах го, за да проверя дали 8099.99975 е закръглено до 8100, когато е написано като IEEE 754 с плаваща единица с единична точност. За моя изненада Java го преобразува в 8099.9995, когато е написан като плаващ литерал (8099.99975f). Проверих изчисленията си и стандарта IEEE отново, но не можах да намеря никакви грешки. 8100 е също толкова далеч от 8099.99975, колкото 8099.9995, но последният бит от 8100 е 0, което трябва да го направи правилното представяне.

Така че проверих спецификацията на езика Java, за да видя дали съм пропуснал нещо. След бързо търсене открих две неща:

  • #P5#
  • #P6#

Забелязах тук, че нищо не е казано за float литералите. Така че си помислих, че float литералите може би са просто двойни, които, когато се преобразуват към float, се закръглят до нула, подобно на прехвърлянето на float към int. Това би обяснило защо 8099.99975f е закръглено до нула.

Написах малката програма, която можете да видите по-горе, за да проверя моята теория и наистина открих, че при добавяне на два плаващи литерала, които трябва да доведат до 8100, се изчислява правилният float. (Тук имайте предвид, че 8099.9995 и 0.00025 могат да бъдат представени точно като единични плаващи числа, така че няма закръгляване, което може да доведе до различен резултат) Това ме обърка, тъй като нямаше много смисъл за мен, че плаващите литерали и изчислените плаващи числа се държат по различен начин, така че аз копах в спецификацията на езика още малко и намерих това:

Литералът с плаваща запетая е от тип float, ако има суфикс с ASCII буква F или f [...]. Елементите на типовете float [...] са тези стойности, които могат да бъдат представени с помощта на IEEE 754 32-битови [...] двоични формати с плаваща запетая с единична точност.

Това в крайна сметка заявява, че литералите трябва да бъдат закръглени в съответствие със стандарта IEEE, който в този случай е до 8100. Така че защо е 8099.9995?


person IchBinKeinBaum    schedule 25.11.2013    source източник
comment
println е мръсен лъжец - подозирам, че неочаквано закръгля и изхвърля резултатите/наблюденията от тестовия случай. Опитайте с явен String.format.   -  person user2864740    schedule 25.11.2013
comment
Тоест println(String.format("%.20f", v)) трябва да доведе до представителни стойности на дисплея.   -  person user2864740    schedule 25.11.2013
comment
Ако println беше проблемът, пак бих очаквал 8099.9995f + 0.00025f == 8099.99975f да е вярно. И знам, че аритметиката с плаваща запетая не е точна. Знам, че резултатът ми няма да бъде точно 8099,99975. Но първите две числа са представени точно. Не са закръглени или нещо подобно. Не виждам защо добавянето на две числа не води до същия резултат като преобразуването на десетичния в единичен float.   -  person IchBinKeinBaum    schedule 25.11.2013
comment
Ключът към разбирането на този вид неща е да погледнете точните стойности на плаващите стойности. Опитайте да отпечатате new BigDecimal(8099.99975f).toString() и т.н. Конструкторът BigDecimal и неговият toString() са точни.   -  person Patricia Shanahan    schedule 25.11.2013
comment
Тук имаме още един въпрос, неправилно затворен като дубликат. Този въпрос конкретно включва не само поведението на аритметичната система с плаваща запетая, но и как Java показва стойности с плаваща запетая. Последното не е разгледано в предполагаемия първоначален въпрос или отговорите му.   -  person Eric Postpischil    schedule 25.11.2013
comment
8099.99975f се преобразува в най-близката стойност с плаваща запетая, което е число, много малко по-голямо от 8099.9995, поради което е избрано вместо 8100. Въпреки това, когато Java println преобразува стойности с плаваща запетая в десетични, обикновено не показва всички цифри. Той показва точно толкова цифри, че преобразуването обратно в плаваща запетая показва същата стойност. Така виждате само „8099.995“, но действителната стойност е малко по-голяма.   -  person Eric Postpischil    schedule 25.11.2013
comment
@Eric Съгласен съм напълно и с двата ви коментара. Този друг въпрос абсолютно НЕ е дубликат на този. Изглежда, че хората, които затвориха въпроса, не си направиха труда да прочетат и двата въпроса. Гласувах за повторно отваряне и призовавам другите да направят същото. Освен това, ако този въпрос НАПРАВИ бъде отворен отново, моля, преобразувайте втория си коментар в отговор и аз ще го гласувам в полза. Въпреки че е подобен на моя отговор, той дава малко по-различна гледна точка, която смятам за ценна.   -  person Dawood ibn Kareem    schedule 25.11.2013
comment
@DavidWallace: Бих предложил просто да добавите към вашия добър отговор. Можете да дадете конкретното правило на Java и да обясните как да видите пълната стойност (или по друг начин да промените дисплея от стандартния Java). Това дава на читателите знания, които могат да използват, за да разберат, изследват и контролират включеното поведение. Мога да предоставя правилото на Java, ако желаете; Цитирах го от спецификацията в стари отговори, но в момента съм на малко устройство и не искам да го копая по този начин.   -  person Eric Postpischil    schedule 25.11.2013
comment
@EricPostpischil Ще му дам малко време. Ако здравият разум надделее и този въпрос бъде повдигнат отново, мисля, че отговорът от вас ще бъде добре дошло допълнение към двата, които вече са тук. Ако не бъде отворен отново след ден или два, ще редактирам забележките ви в отговора си, като, разбира се, ще ви отдам дължимото.   -  person Dawood ibn Kareem    schedule 25.11.2013
comment
@DavidWallace: Ето Java toString документация за типа float, който се използва за println. Формулировката е „Трябва да има поне една цифра, която да представлява дробната част, и отвъд това толкова много, но само толкова, повече цифри, колкото са необходими за уникалното разграничаване на стойността на аргумента от съседните стойности от тип float.“   -  person Eric Postpischil    schedule 25.11.2013


Отговори (2)


Ключовият момент, който трябва да разберете, е, че стойността на число с плаваща запетая може да бъде разработена по два различни начина, които като цяло не са еднакви.

  • Това е стойността, на която битовете в числото с плаваща запетая дават точното двоично представяне.
  • Има „десетична стойност на показване“ на число с плаваща запетая, което е числото с най-малко десетични знаци, което е по-близо до това число с плаваща запетая от всяко друго число.

За да разберете разликата, помислете за числото, чийто показател е 10001011 и чието значение е 1,11111010001111111111111. Това е точното двоично представяне на 8099.99951171875. Но десетичната стойност 8099.9995 има по-малко десетични знаци и е по-близо до това число с плаваща запетая, отколкото до всяко друго число с плаваща запетая. Следователно 8099,9995 е стойността, която ще се покаже, когато отпечатате това число.

Имайте предвид, че това конкретно число с плаваща запетая е следващото най-ниско след 8100.

Сега разгледайте 8099.99975. То е малко по-близо до 8099.99951171875, отколкото до 8100. Следователно, за да го представи с плаваща запетая с единична точност, Java ще избере числото с плаваща запетая, което е точното двоично представяне на 8099.99951171875. Ако се опитате да го отпечатате, ще видите 8099.9995.

И накрая, когато правите 8099.9995 + 0.00025 с плаваща запетая с единична точност, включените числа са точните двоични представяния на 8099.99951171875 и 0.0002499999827705323696136474609375. Но тъй като последното е малко повече от 1/2^12, резултатът от събирането ще бъде по-близък до 8100, отколкото до 8099.99951171875, и така ще бъде закръглен нагоре, а не надолу в края, което го прави 8100.

person Dawood ibn Kareem    schedule 25.11.2013
comment
Уф, целият проблем беше, че използвах преобразувана двоична в десетична, за да търся следващите най-близки числа до 8100. Каза ми, че това е 8099,9995. Очаквах това да е точната стойност, а не някаква неточна, а по-кратка. Така че цялото ми разсъждение се основаваше на предположението, че 8099,9995 всъщност може да бъде представено точно. Благодаря, че отбеляза, че това не е така. - person IchBinKeinBaum; 25.11.2013
comment
Забележете, че 0,9995 = 1999/2000. Но тъй като 2000 не е степен на две, тази дроб не може да има крайно двоично представяне. С други думи, не може да има число с плаваща запетая с НИКАКВО ниво на точност, което е точно 0,9995. Добавянето на 8099 няма значение за това. Без значение каква точност използвате, число с плаваща запетая, което е приблизително 8099,9995, никога не може да бъде ТОЧНО това число. - person Dawood ibn Kareem; 25.11.2013

Десетичната стойност 8099.99975 има девет значещи цифри. Това е повече, отколкото може да бъде представено точно в float. Ако използвате инструмента за анализ с плаваща запетая в CUNY ще видите, че двоичното представяне, най-близко до 8099.9995, е 45FD1FFF. Когато се опитате да добавите 0.00025 страдате от „загуба на значимост“. За да не се загубят значими (леви) цифри на по-голямото число, значимото на по-малкото трябва да се измести надясно, за да съответства на мащаба (експонента) на по-голямото. Когато това се случи, стойността му става НУЛА, тъй като се измества от десния край на регистъра.

Decimal     Exponent        Significand
---------   --------------  -------------------------
8099.9995   10001011 (+12)  1.11111010001111111111111
   0.00025  01110011 (-12)  1.00000110001001001101111

За да ги подреди за добавяне, вторият трябва да измести надясно 24 бита, но има само 23 бита в сигнифидата на плаваща единица с единична точност. Значимото изчезва, оставяйки нула, така че добавянето няма ефект.

Ако искате това да работи, преминете към аритметика с двойна точност.

person Jim Garrison    schedule 25.11.2013
comment
Както казах, истинският ми въпрос е защо 8099.99975f не се преобразува в 8100? Но ти всъщност посочи нещо различно, за което дори не се бях замислял: 8099.9995f + 0.00025f трябва да е 8099.9995f, нали? Както казахте, по-ниската стойност се измества, докато стане нула. Тогава защо Java ми казва, че е 8100? Използва ли двойни, дори ако изрично му кажа да използва плаващи числа? - person IchBinKeinBaum; 25.11.2013
comment
Този отговор е грешен; добавянето на .00025f към 8099.9995f произвежда 8100, а не стойността за 8099.9995f. - person Eric Postpischil; 25.11.2013
comment
Изходният текст 8099.9995f произвежда стойност, много малко по-голяма от 8099.9995. Тогава добавянето на .00025f произвежда (математически, преди закръгляване с плаваща запетая) стойност, по-близка до 8100, отколкото до стойност близо до 8099,9995. След това закръгляването произвежда 8100. Битовете „под“ значимото не се игнорират; те влияят на закръгляването. - person Eric Postpischil; 25.11.2013