Почему C и Java Round плавают по-разному?

Рассмотрим число с плавающей запятой 0.644696875. Давайте преобразуем его в строку с восемью десятичными знаками, используя Java и C:

Java

import java.lang.Math;
public class RoundExample{
     public static void main(String[] args){
        System.out.println(String.format("%10.8f",0.644696875));
     }
}

результат: 0.6446968 8

Попробуйте сами: http://tpcg.io/oszC0w

C

#include <stdio.h>

int main()
{
    printf("%10.8f", 0.644696875); //double to string
    return 0;
}

результат: 0.6446968 7

Попробуйте сами: http://tpcg.io/fQqSRF

Вопрос

Почему последняя цифра отличается?

Фон

Номер 0.644696875 не может быть представлен точно как номер машины. Он представлен как дробь 2903456606016923/4503599627370496, которая имеет значение 0,6446968749999999

Это по общему признанию крайний случай. Но мне действительно любопытно узнать источник различий.

Связанный: https://mathematica.stackexchange.com/info/204359/is-numberform-double-rounding-numbers

Ответ 1

Заключение

Спецификация Java требует сложного двойного округления в этой ситуации. Число 0,6446968749999999470645661858725361526012420654296875 сначала преобразуется в 0,644696875, а затем округляется до 0,64469688.

Напротив, реализация C просто округляет 0,6446968749999999470645661858725361526012420654296875 до восьми цифр, что дает 0,64469687.

Отборочные

Для Double Java использует базовый 64-разрядный двоичный код с плавающей точкой IEEE-754. В этом формате значение, ближайшее к числу в исходном тексте, 0,644696875, равно 0,6446968749999999470645661858725361526012420654296875, и я считаю, что это фактическое значение, которое должно быть отформатировано с помощью String.format("%10.8f",0.644696875). 1

Что говорит спецификация Java

Документация для форматирования с типом Double и f гласит:

… Если точность меньше количества цифр, которое должно появиться после десятичной точки в строке, возвращаемой Float.toString(float) или Double.toString(double) соответственно, то значение будет округлено с использованием алгоритма округления до половины. В противном случае для достижения точности можно добавить нули...

Давайте рассмотрим "строку, возвращаемую… Double.toString(double)". Для номера 0,6446968749999999470645661858725361526012420654296875 эта строка равна 0,644696875. Это связано с тем, что в спецификации Java говорится, что toString выдает достаточно десятичных цифр, чтобы однозначно различать число в наборе значений Double, а в "0,644696875" достаточно только цифр в этом случае. 2

Это число имеет девять цифр после десятичной точки, и "%10.8f" запрашивает восемь, поэтому в приведенном выше отрывке говорится, что "значение" округлено. Какое значение это означает - фактический операнд format, который равен 0,6446968749999999470645661858725361526012420654296875, или та строка, которую он упоминает, "0,644696875"? Поскольку последнее не является числовым значением, я ожидал бы, что "значение" будет означать первое. Тем не менее, второе предложение говорит: "В противном случае [то есть, если запрашивается больше цифр], могут добавляться нули…" Если бы мы использовали фактический операнд из format, мы бы показывали его цифры, а не использовали нули. Но если мы возьмем строку в качестве числового значения, ее десятичное представление будет иметь только нули после цифр, показанных в ней. Так что, похоже, это и есть толкование, и реализации Java, похоже, соответствуют этому.

Итак, чтобы отформатировать это число с помощью "%10.8f", мы сначала конвертируем его в 0,644696875, а затем округляем его, используя правило округления до половины, что дает 0,64469688.

Это плохая спецификация, потому что:

  • Требуется два округления, что может увеличить ошибку.
  • Округления происходят в трудно предсказуемых и трудно контролируемых местах. Некоторые значения будут округлены после двух десятичных знаков. Некоторые из них будут округлены после 13. Программа не может легко предсказать это или скорректировать его.

(Также жаль, что они написали, что нули "могут быть" добавлены. Почему бы и нет? В противном случае нули добавляются для достижения точности "?" С "может", кажется, что они дают реализации выбор, хотя я подозреваю, что они имели в виду, что "май" основан на том, нужны ли нули для достижения точности, а не на том, решит ли разработчик добавить их.)

Сноска

1 Когда 0.644696875 в исходном тексте преобразуется в Double, я считаю, что результатом должно быть ближайшее значение, представляемое в формате Double. (Я не нашел этого в документации по Java, но она соответствует философии Java, согласно которой реализация должна вести себя одинаково, и я подозреваю, что преобразование выполняется в соответствии с Double.valueOf(String s), который требует этого.) Ближайший Double к 0,644696875 - 0,6446968749999999470645661858725361526012420654296875.

2 При меньшем количестве цифр 0,64469687 из семи цифр недостаточно, поскольку ближайшее к нему значение Double составляет 0,6446968 699999999774519210404832847416400909423828125. Таким образом, для однозначного различения 0,6446968 749999999470645661858725361526012420654296875 требуется, что необходимо из восьми цифр.

Ответ 2

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

Помните, однако, что float имеет 24 бита для своей дроби, которая составляет ~ 7,22 десятичных цифр [log10 (2) * 24], и первые 7 цифр согласуются между ними, так что это только последние несколько младших значащих битов, которые являются отличается.

Добро пожаловать в увлекательный мир математики с плавающей точкой, где 2 + 2 не всегда равно 4.