Почему числа с плавающей запятой печатаются по-разному?

Это обычное знание того, что (большинство) чисел с плавающей запятой точно не сохраняются (когда используется формат IEEE-754). Поэтому не стоит этого делать:

0.3 - 0.2 === 0.1; // very wrong

... поскольку это приведет к false, если не используется какой-либо конкретный тип/класс произвольной точности (BigDecimal в Java/Ruby, BCMath в PHP, Math::BigInt/Math::BigFloat в Perl, чтобы назвать несколько).

Однако мне интересно, почему, когда вы пытаетесь напечатать результат этого выражения, 0.3 - 0.2, языки сценариев (Perl и PHP) дают 0.1, но "виртуальные машины" (Java, JavaScript и Erlang) дают нечто похожее на 0.09999999999999998 вместо?

И почему это также непоследовательно в Ruby? версия 1.8.6 (codepad) дает 0.1, версия 1.9.3 (ideone) дает 0.0999...

Ответы

Ответ 1

Номера с плавающей запятой печатаются по-разному, потому что печать выполняется для разных целей, поэтому сделаны различные варианты о том, как это сделать.

Печать числа с плавающей запятой - это операция преобразования: значение, закодированное во внутреннем формате, преобразуется в десятичную цифру. Тем не менее, есть варианты относительно деталей преобразования.

(A) Если вы делаете точную математику и хотите увидеть фактическое значение, представленное внутренним форматом, то преобразование должно быть точным: оно должно содержать десятичную цифру, которая имеет точно такую ​​же значение как вход. (Каждое число с плавающей запятой представляет собой ровно одно число. Число с плавающей запятой, как определено в стандарте IEEE 754, не представляет интервала.) Иногда это может потребовать создания очень большого количества цифр.

(B) Если вам не нужно точное значение, но вам нужно конвертировать туда и обратно между внутренним форматом и десятичным, вам нужно точно преобразовать его в десятичную цифру (и точно ) достаточно, чтобы отличить его от любого другого результата. То есть вы должны создать достаточное количество цифр, чтобы результат отличался от того, что вы получили бы, преобразовывая числа, которые смежны во внутреннем формате. Это может потребовать создания большого количества цифр, но не так много, чтобы быть неуправляемым.

(C) Если вы хотите только дать читателю смысл номера и не нужно создавать точное значение, чтобы ваше приложение функционировало по желанию, тогда вам нужно только для создания как можно большего количества цифр для вашего конкретного приложения.

Какое из этих преобразований должно выполняться?

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

(A) требует тщательного кода, а некоторые языки или их реализации не предоставляют или не гарантируют, чтобы это поведение выполнялось.

(B) требуется Java, я считаю. Однако, как мы видели в недавнем вопросе, это может привести к неожиданному поведению. (65.12 печатается как "65.12", потому что у последнего достаточно цифр, чтобы отличить его от близлежащих значений, но 65.12-2 печатается как "63.120000000000005", потому что между ним и 63.12 имеется другое значение с плавающей запятой, поэтому вам нужно дополнительные цифры, чтобы отличить их.)

(C) - это то, что некоторые языки используют по умолчанию. Это, по сути, неправильно, поскольку ни одно значение для того, сколько цифр для печати может быть подходящим для всех приложений. Действительно, мы видели на протяжении десятилетий, что это способствует продолжению неправильных представлений о плавающей точке, в основном, скрывая истинные ценности. Это, однако, легко реализовать и, следовательно, привлекательно для некоторых разработчиков. В идеале, язык должен по умолчанию печатать правильное значение числа с плавающей запятой. Если нужно отобразить меньшее количество цифр, количество цифр должно быть выбрано только разработчиком приложения, и мы надеемся, что в нем будет учтено соответствующее количество цифр для получения желаемых результатов.

Хуже того, некоторые языки в дополнение к тому, что они не отображают фактическое значение или достаточно цифр, чтобы отличить его, даже не гарантируют правильность приведенных цифр (например, значение, которое вы получите, округляя точное значение на количество отображаемых цифр). При программировании в реализации, которая не дает гарантии об этом поведении, вы не занимаетесь разработкой.

Ответ 2

Как и для php, вывод связан с настройками ini:

ini_set('precision', 15);
print 0.3 - 0.2; // 0.1

ini_set('precision', 17);
print 0.3 - 0.2; //0.099999999999999978 

Это может быть причиной других языков

Ответ 3

PHP автоматически округляет число до произвольной точности.

Число с плавающей запятой в целом неточно (как вы отметили), и вам нужно использовать специфичную для языка функцию round(), если вам нужно сравнить только несколько десятичных знаков. В противном случае возьмите абсолютное значение уравнения и проверьте, что они находятся в заданном диапазоне.

Пример PHP из php.net:

$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;
if(abs($a - $b) < $epsilon) {
  echo "true";
}

Что касается проблемы с Ruby, они, похоже, используют разные версии. Codepad использует 1.8.6, в то время как Ideaone использует 1.9.3, но он скорее связан с конфигурацией где-то.

Ответ 4

Если мы хотим это свойство

  • каждые два разных поплавка имеют другое печатное представление

Или еще более сильный, полезный для REPL

  • печатное представление должно быть повторно интерпретировано без изменений

Затем я вижу 3 решения для печати float/double с внутренним представлением базы 2 в базу 10

  • напечатать EXACT-представление.
  • напечатать достаточно десятичных цифр (с правильным округлением)
  • напечатать кратчайшее десятичное представление, которое может быть переинтерпретировано без изменений

Так как в базе два, число с плавающей запятой является an_integer * 2 ^ an_exponent, его базовое 10 точное представление имеет конечное число цифр.
К сожалению, это может привести к очень длинным строкам... Например, 1.0e-10 представляется в точности как 1.0000000000000000364321973154977415791655470655996396089904010295867919921875e-10

Решение 2 легко, вы используете printf с 17 цифрами для IEEE-754 double...
Недостаток: он не точный, ни самый короткий! Если вы входите в 0.1, вы получаете +0,100000000000000006

Решение 3 является лучшим для языков REPL, если вы введете 0,1, он печатает 0,1
К сожалению, он не найден в стандартных библиотеках (позор).
По крайней мере, Scheme, Python и недавний Squeak/Pharo Smalltalk делают все правильно, я думаю, что Java тоже.

Ответ 5

Что касается Javascript, base2 используется для внутренних расчетов.

> 0.2 + 0.4
0.6000000000000001

Для этого Javascript может передавать только четные числа, если результирующее число base2 не является периодическим.

0,6 является 0.10011 10011 10011 10011 ... в base2 (периодическом), тогда как 0.5 не является и поэтому правильно напечатано.