Java пытается маскировать неточную природу double-ов
От: vsb Казахстан  
Дата: 21.02.22 05:21
Оценка: 8 (3) +2 :))
Вчера Java заставила меня немножко посомневаться в своей компетентности.

Известно, что число 0.3 не представимо в двоичном виде. То, как оно представляется, на самом деле представляет число 0.299999999999999988897769753748434595763683319091796875000000 (да, я заморочился и проверил). Если написать на C++ printf("%.60f", 0.3), то именно это число и будет напечатано. Если написать printf("%.17f", 0.3), то будет напечатано число 0.29999999999999999.

Если в Java написать printf("%.17f", 0.3), то напишется 0.30000000000000000. Если "%0.60f", то будет 0.3000.... Т.е. жава тупо врёт и округляет значение до 0.3 при конвертации в строку, прежде чем выполнять финальное форматирование и округление.

Конечно понятно, что природу double-ов не скроешь такими наивными трюками и 0.1+0.2 уже выдаёт 0.30000000000000004. Слава богу, уж это не пытаются округлять. Но всё же лично у меня это всё вызвало неприятный сюрприз, т.к. пришлось потратить некоторое время, прежде чем я понял, что Java тупо занимается читингом.

Судя по всему любое число, представимое в десятичной нотации вроде 0.000123 они при toString в первую очередь переводят в подобную нотацию и потом округляют уже это десятичное число. На мой взгляд это грязный трюк, который скрывает истинную природу double-ов и провоцирует их использовать там, где их использовать не стоит, т.к. создаёт начальную видимость того, что всё работает правильно.

А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.
Отредактировано 21.02.2022 5:22 vsb . Предыдущая версия .
Re: Java пытается маскировать неточную природу double-ов
От: sambl74 Россия  
Дата: 21.02.22 05:31
Оценка: :)
Здравствуйте, vsb, Вы писали:

vsb>А вы как думаете? Может я не прав и это норма в современных языках?


В современных языках parseInt(Number("0.0000005")) выдаёт 5
Re: Java пытается маскировать неточную природу double-ов
От: Alexander G Украина  
Дата: 21.02.22 05:55
Оценка: 3 (2) :)
Здравствуйте, vsb, Вы писали:

vsb>А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.


Современные тенданции — специально не печатать незначащие десятичные знаки, напечатать их лишь столько, чтобы при обратной конвертации из строки попасть туда же.
Подход printf неправильный, устаревший. Следует использовать std::format.

#include <format>
#include <iostream>

int main()
{
    printf("%.60f\n", 1.0/3); // 0.333333333333333314829616256247390992939472198486328125000000
    std::cout << std::format("{:60}\n", 1.0/3); // 0.3333333333333333
    printf("%.60f\n", 0.3); // 0.299999999999999988897769753748434595763683319091796875000000
    std::cout << std::format("{:60}\n", 0.3); // 0.3
}


Да, кроме std::format так же себя ведёт и std::to_chars
Русский военный корабль идёт ко дну!
Отредактировано 21.02.2022 7:55 Alexander G . Предыдущая версия .
Re: Java пытается маскировать неточную природу double-ов
От: vaa  
Дата: 21.02.22 06:02
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Вчера Java заставила меня немножко посомневаться в своей компетентности.


vsb>Известно, что число 0.3 не представимо в двоичном виде. То, как оно представляется, на самом деле представляет число 0.299999999999999988897769753748434595763683319091796875000000 (да, я заморочился и проверил).


#include <stdio.h>
void main(){
    float x = 0.3;
    printf("%.60f\n", x);   //0.300000011920928960000000000000000000000000000000000000000000                                                                                                                                                     
    printf("%.60f\n", 0.3); //0.299999999999999990000000000000000000000000000000000000000000 
}


в тоже время, в F#(dotnet x64)
0.3 остается 0.3

а cl вообще можно управлять точностью. как настроишь такие и будут значения.
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 06:48
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Вчера Java заставила меня немножко посомневаться в своей компетентности.


vsb>Известно, что число 0.3 не представимо в двоичном виде. То, как оно представляется, на самом деле представляет число 0.299999999999999988897769753748434595763683319091796875000000 (да, я заморочился и проверил).


Последние 6 нулей можно было бы и срезать. Не принципиально, но для совсем уж строгости.

vsb>А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.


Я полностью согласен, что подход Java некорректен.

Вот ещё голос в ту же сторону.

The Java specification requires a troublesome double rounding in this situation.

Тут.

UPD: Судя по свежему dotnet-sdk, в дотнете эту глупость не повторили.
The God is real, unless declared integer.
Отредактировано 21.02.2022 7:21 netch80 . Предыдущая версия .
Re[2]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 06:50
Оценка:
Здравствуйте, sambl74, Вы писали:

S>В современных языках parseInt(Number("0.0000005")) выдаёт 5


Достаточно просто parseInt(0.00000005)
The God is real, unless declared integer.
Re[2]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 07:04
Оценка:
Здравствуйте, Alexander G, Вы писали:

vsb>>А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.


AG>Современные тенданции — специально не печатать незначащие десятичные знаки, напечатать их лишь столько, чтобы при обратной конвертации из строки попасть туда же.


Это если запрос явно или неявно подчёркивает печатать ровно столько знаков, сколько надо для точного представления.
Запросы вида %<n>.f к этому не относятся аж никак.

AG>Подход printf неправильный, устаревший. Следует использовать std::format.


Не у всех будет C++20 даже в пару ближайших лет.
The God is real, unless declared integer.
Re[2]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 07:29
Оценка: +1
Здравствуйте, vaa, Вы писали:

vaa> float x = 0.3;

vaa> printf("%.60f\n", x); //0.300000011920928960000000000000000000000000000000000000000000
vaa> printf("%.60f\n", 0.3); //0.299999999999999990000000000000000000000000000000000000000000
vaa>}

1. Это другой вопрос: float имеет меньшую точность и (при адекватном парсере) 0.3 имеет ближайшее представление чуть выше, чем 0.3 (а не чуть ниже, как у double).

2. Это чем вы таким извращённым компилировали? У меня так не получается:

0.300000011920928955078125000000000000000000000000000000000000
0.299999999999999988897769753748434595763683319091796875000000


Это одинаково на Ubuntu 20.04 (с glibc) и FreeBSD 12.3.

vaa>в тоже время, в F#(dotnet x64)

vaa>0.3 остается 0.3

Чем именно выводили? Это существенно. Покажите точный код.

vaa>а cl вообще можно управлять точностью. как настроишь такие и будут значения.


В каком смысле "настроишь"?
The God is real, unless declared integer.
Re[3]: Java пытается маскировать неточную природу double-ов
От: Alexander G Украина  
Дата: 21.02.22 07:54
Оценка:
Здравствуйте, netch80, Вы писали:

N>Здравствуйте, Alexander G, Вы писали:


vsb>>>А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.


AG>>Современные тенданции — специально не печатать незначащие десятичные знаки, напечатать их лишь столько, чтобы при обратной конвертации из строки попасть туда же.


N>Это если запрос явно или неявно подчёркивает печатать ровно столько знаков, сколько надо для точного представления.

N>Запросы вида %<n>.f к этому не относятся аж никак.

Верно, что printf определён так, что печатать столько знаков, сколько получится из алгоритма конвертации.
Но я думаю, печатать столько знаков, сколько действительно есть, и не больше — более правильный подход, который должен применяться по умолчанию, если не вообще всегда.

Вряд ли printf поменяют. но все новые форматтеры уже без лишних знаков. C++17 to_chars даже.

AG>>Подход printf неправильный, устаревший. Следует использовать std::format.


N>Не у всех будет C++20 даже в пару ближайших лет.


Понимаю, я лишь указал к чему идёт.
Но кстати std::to_chars c С++17.
И библиотека fmt портируема.
Русский военный корабль идёт ко дну!
Re[3]: Java пытается маскировать неточную природу double-ов
От: vaa  
Дата: 21.02.22 08:43
Оценка:
Здравствуйте, netch80, Вы писали:

N>Здравствуйте, vaa, Вы писали:


vaa>> float x = 0.3;

vaa>> printf("%.60f\n", x); //0.300000011920928960000000000000000000000000000000000000000000
vaa>> printf("%.60f\n", 0.3); //0.299999999999999990000000000000000000000000000000000000000000
vaa>>}

N>1. Это другой вопрос: float имеет меньшую точность и (при адекватном парсере) 0.3 имеет ближайшее представление чуть выше, чем 0.3 (а не чуть ниже, как у double).


N>2. Это чем вы таким извращённым компилировали? У меня так не получается:

Tiny C Compiler
N>
N>0.300000011920928955078125000000000000000000000000000000000000
N>0.299999999999999988897769753748434595763683319091796875000000
N>


N>Это одинаково на Ubuntu 20.04 (с glibc) и FreeBSD 12.3.


vaa>>в тоже время, в F#(dotnet x64)

vaa>>0.3 остается 0.3

N>Чем именно выводили? Это существенно. Покажите точный код.

поправка, и шарп неточен:
> System.String.Format("{0:F60}", 0.3f);;
val it: string =
  "0,300000011920928955078125000000000000000000000000000000000000"


vaa>>а cl вообще можно управлять точностью. как настроишь такие и будут значения.


N>В каком смысле "настроишь"?

возможно я ошибься и у меня была проблема с целыми. давно не играл с лиспом. но возможности там действительно впечатляют.

http://lisper.ru/pcl/numbers-characters-and-strings
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re[4]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 09:18
Оценка: 2 (1)
Здравствуйте, vaa, Вы писали:

N>>2. Это чем вы таким извращённым компилировали? У меня так не получается:

vaa>Tiny C Compiler

Хм, похоже, у рантайма внутри какое-то явное усечение до точности float, даже если формально работает с double. Если оно работает с FPU, оно криво выставило режим.

N>>Чем именно выводили? Это существенно. Покажите точный код.

vaa>поправка, и шарп неточен:
vaa>
>> System.String.Format("{0:F60}", 0.3f);;
vaa>val it: string =
vaa>  "0,300000011920928955078125000000000000000000000000000000000000"
vaa>


Так это как раз нормально: 0.3f чуть больше чем точное 0.3 (а 0.3 double — меньше).
Внутри при показе оно, наверно, конвертируется в double, но точность от этого не возвращается (double хранит то, что было лучшим приближением для float).

vaa>>>а cl вообще можно управлять точностью. как настроишь такие и будут значения.

N>>В каком смысле "настроишь"?
vaa>возможно я ошибься и у меня была проблема с целыми. давно не играл с лиспом. но возможности там действительно впечатляют.

vaa>http://lisper.ru/pcl/numbers-characters-and-strings


Ааа, cl это Common LISP... там, да, свой мир, весьма интересный.
The God is real, unless declared integer.
Re: Java пытается маскировать неточную природу double-ов
От: CreatorCray  
Дата: 21.02.22 10:13
Оценка:
Здравствуйте, vsb, Вы писали:
vsb>Известно, что число 0.3 не представимо в двоичном виде. То, как оно представляется, на самом деле представляет число 0.299999999999999988897769753748434595763683319091796875000000 (да, я заморочился и проверил).
Да чего там заморачиваться то? Внутреннее представление (0x3fd3333333333333) даёт:
((1 << 52) | 0x3333333333333) / ((1 << 52) << (1023 — 0x3fd)) == 0x13333333333333 / 0x40000000000000 == то, что у тебя получилось
... << RSDN@Home 1.3.110 alpha 5 rev. 62>>
Забанили по IP, значит пора закрыть эту страницу.
Всем пока
Re: Java пытается маскировать неточную природу double-ов
От: Mr.Delphist  
Дата: 21.02.22 10:42
Оценка: +2
Здравствуйте, vsb, Вы писали:

vsb>Если в Java написать printf("%.17f", 0.3), то напишется 0.30000000000000000. Если "%0.60f", то будет 0.3000.... Т.е. жава тупо врёт и округляет значение до 0.3 при конвертации в строку, прежде чем выполнять финальное форматирование и округление.


vsb>А вы как думаете? Может я не прав и это норма в современных языках? Лично мне ближе подход C++, который показывает математически точное значение при заданной точности.


А какую задачу решаем? Если нужна прямо вот точность-точность, то для этого есть всякая Big Math (BigInteger, BigDecimal). Если же хочется оставаться на стандартных типах, то принимаем условия игры про диапазоны значений и точность.
Re[3]: Java пытается маскировать неточную природу double-ов
От: AeroSun  
Дата: 21.02.22 11:05
Оценка: :))
Здравствуйте, netch80, Вы писали:

N>Не у всех будет C++20 даже в пару ближайших лет.


Это всё полностью зависит от разработчиков
Решили мучаться — пусть мучаются
Re[2]: Java пытается маскировать неточную природу double-ов
От: vsb Казахстан  
Дата: 21.02.22 11:58
Оценка:
Здравствуйте, CreatorCray, Вы писали:

vsb>>Известно, что число 0.3 не представимо в двоичном виде. То, как оно представляется, на самом деле представляет число 0.299999999999999988897769753748434595763683319091796875000000 (да, я заморочился и проверил).

CC>Да чего там заморачиваться то? Внутреннее представление (0x3fd3333333333333) даёт:
CC>((1 << 52) | 0x3333333333333) / ((1 << 52) << (1023 — 0x3fd)) == 0x13333333333333 / 0x40000000000000 == то, что у тебя получилось

Ну я примерно так и считал. 1/4 * (1 + 3/16 + 3/(16^2) + ... + 3/(16^13)).
Re[2]: Java пытается маскировать неточную природу double-ов
От: vsb Казахстан  
Дата: 21.02.22 12:03
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>А какую задачу решаем? Если нужна прямо вот точность-точность, то для этого есть всякая Big Math (BigInteger, BigDecimal). Если же хочется оставаться на стандартных типах, то принимаем условия игры про диапазоны значений и точность.


Я не в курсе условий этой игры.

Предположу. Пусть у нас есть три последовательно идущие double значения: a, b, c. У которых есть точные однозначные значения, выражаемые их представлением в IEEE-формате.

Разделим границы: bMin = (a + b) / 2, bMax = (b + c) / 2. Тоже точные однозначные значения. Т.е. есть последовательные числа a, bMin, b, bMax, c. bMin и bMax в double уже не влазят, но на бумажке написать можно.

И далее считаем, что окружение имеет право менять b на любое значение в рамках между bMin и bMax в операциях toString (и, наверное, ещё в каких-то). Например на то, длина которого в десятичной записи минимальна.

Верно ли я выразил эти правила? В рамках этих правил Java может быть и права.
Отредактировано 21.02.2022 12:04 vsb . Предыдущая версия . Еще …
Отредактировано 21.02.2022 12:04 vsb . Предыдущая версия .
Re[4]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 12:09
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>>>Современные тенданции — специально не печатать незначащие десятичные знаки, напечатать их лишь столько, чтобы при обратной конвертации из строки попасть туда же.

N>>Это если запрос явно или неявно подчёркивает печатать ровно столько знаков, сколько надо для точного представления.
N>>Запросы вида %<n>.f к этому не относятся аж никак.
AG>Верно, что printf определён так, что печатать столько знаков, сколько получится из алгоритма конвертации.

Да, и к тому некоторые умолчания в случае незаданной точности.

AG>Но я думаю, печатать столько знаков, сколько действительно есть, и не больше — более правильный подход, который должен применяться по умолчанию, если не вообще всегда.


Это уже вопрос целевого использования. Где-то нужна как раз принудительная конвертация к заданному уровню точности.
Можно сравнить с Python: repr() (окончательно устоялось в 3.0), да, выбирает самую короткую форму из тех, которые обратно превращаются в то же двоичное значение при стандартном умолчательном округлении (round half to even). Но варианты с указанием точности — округляют именно до этой точности, даже если дают лишние цифры.

AG>Вряд ли printf поменяют. но все новые форматтеры уже без лишних знаков. C++17 to_chars даже.


А доку прочитать? У to_chars есть вариант с "the shortest representation requirement", а есть с явной точностью.

AG>>>Подход printf неправильный, устаревший. Следует использовать std::format.


N>>Не у всех будет C++20 даже в пару ближайших лет.


AG>Понимаю, я лишь указал к чему идёт.


Идёт не к тому, что этот вариант основной, а всего лишь что он полезен и доступен.

AG>Но кстати std::to_chars c С++17.


См. выше.
И кстати именно to_chars даже в GCC10 неполная (сейчас проверил, для double не сделали).
The God is real, unless declared integer.
Re[3]: Java пытается маскировать неточную природу double-ов
От: Mr.Delphist  
Дата: 21.02.22 12:15
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>bMin и bMax в double уже не влазят, но на бумажке написать можно.


Собственно, это даже не касается Java — это общие моменты вычислительной матеатики.

vsb>И далее считаем, что окружение имеет право менять b на любое значение в рамках между bMin и bMax в операциях toString (и, наверное, ещё в каких-то). Например на то, длина которого в десятичной записи минимальна.


JVM не держит в голове контекст "кого с кем складывали" — у него просто на руках конкретная ячейка в том самом IEEE-формате. И в строку оно будет приведено, исходя из format specifiers — никакой магии или "грязных трюков".
Re[3]: Java пытается маскировать неточную природу double-ов
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 21.02.22 12:38
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>И далее считаем, что окружение имеет право менять b на любое значение в рамках между bMin и bMax в операциях toString (и, наверное, ещё в каких-то). Например на то, длина которого в десятичной записи минимальна.


vsb>Верно ли я выразил эти правила? В рамках этих правил Java может быть и права.


По сути она именно этим и занимается.

Проблема в следующих вещах:

1. В таком варианте рассказывать про хвостовые нули некорректно. Их там реально нет, и оформлять значение в виде 0.300000000000000000000000000000000000000000000000000000 или 0.300000000000000040000000000000000000000000000000000000 — давать ложное представление.

2. Безальтернативность подобного режима идёт, возможно, на пользу простым юзерам — тем, которые порождали этот вопрос каждые 3 дня, пока все вокруг не обвещались FAQʼами с ответами. Если корректировка до ближайших значений корректно делается за них, то вопроса нет.
Но она резко усложняет жизнь тем, кто не хочет такого и хочет видеть настоящие значения.
Если бы сделали, например, в дополнение к %f какой-нибудь %⊨f, который такого не делает...
The God is real, unless declared integer.
Re: Java пытается маскировать неточную природу double-ов
От: halo Украина  
Дата: 21.02.22 16:26
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Судя по всему любое число, представимое в десятичной нотации вроде 0.000123 они при toString в первую очередь переводят в подобную нотацию и потом округляют уже это десятичное число. На мой взгляд это грязный трюк, который скрывает истинную природу double-ов и провоцирует их использовать там, где их использовать не стоит, т.к. создаёт начальную видимость того, что всё работает правильно.


Я в этом аспекте совершенно не силён, но java.lang.Object.toString() тут ни при чём, как и форматирование. 0.3d — константа с точки зрения языка, и именно в таком виде javac отдаёт значение в пул констант в класс-файла: 0x3FD3333333333333. Почему это считается 0.3d, а не 0.299999999999999988897769753748434595763683319091796875000000 -- не знаю. Причём, сумму 0.1d и 0.2d вычисляет уже как 0.30000000000000004d (это отличие для времени компиляции также предусмотрено в JLS): 0x3FD3333333333334. Поведение, по-моему, не зависит ни от версии Java (8 или 17), ни от присутствия модификатора strictfp. Это видно как и в отладчике при осмотре variadic-аргументов, так и в javap при дизассемблировании class-файла. И, навскидку, так же ведут себя JavaScript (V8), Python 2.7 в своих REPL-ах, в то время как, например, вывод System.Console.WriteLine() в C# округляет сумму в любом случае (даже если "замаскировать" исходные значения за методами, или использовать System.String.Format("{0:F...}") и System.BitConverter.Int64BitsToDouble; "читерит" даже больше чем Java). Т.е., должно быть стандартное правило, допускающее такое поведение.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.