Ошибка округления в два раза в методе .NET Double.ToString

Математически рассмотрим для этого вопроса рациональное число

8725724278030350 / 2**48

где** в знаменателе обозначает возведение в степень, то есть знаменатель2 к48сила (Фракция не в низших терминах, сводится к 2.) Это числоexactly представляемый какSystem.Double, Его десятичное расширение

31.0000000000000'49'73799150320701301097869873046875 (exact)

где апострофы не представляют пропущенные цифры, а просто обозначают границы, где округление до15 соответственно17 цифры должны быть выполнены.

Обратите внимание на следующее: если это число округлено до 15 цифр, результат будет31 (затем тринадцать0s) потому что следующие цифры (49...) начать с4 (имеется в виду раундdown). Но если числоfirst округляется до 17 цифр иthen округляется до 15 цифр, результат может быть31.0000000000001, Это потому, что первое округление округляется путем увеличения49... цифры до50 (terminates) (следующие цифры были73...), и второе округление может затем снова округляться (когда правило округления средней точки говорит «округление от нуля»).

(Конечно, есть еще много чисел с вышеуказанными характеристиками.)

Теперь оказывается, что стандартное строковое представление .NET этого числа"31.0000000000001". The question: Isn't this a bug? Под стандартным строковым представлением мы подразумеваемString производится параметрамиDouble.ToString() метод экземпляра, который, конечно, идентичен тому, что производитсяToString("G").

Интересно отметить, что если вы приведете вышеуказанное число кSystem.Decimal тогда вы получитеdecimal то есть31 именно так! Увидетьэтот вопрос переполнения стека для обсуждения удивительного факта, что кастингDouble вDecimal включает в себя первое округление до 15 цифр. Это означает, что приведение кDecimal делает правильный раунд до 15 цифр, тогда как коллToSting() делает неправильный.

Подводя итог, мы имеем число с плавающей запятой, которое при выводе пользователю31.0000000000001, но при конвертации вDecimal (где29 цифры доступны), становится31 именно так. Это неудачно.

Вот некоторый код C # для вас, чтобы проверить проблему:

static void Main()
{
  const double evil = 31.0000000000000497;
  string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx 

  Console.WriteLine("Exact value (Jon Skeet): {0}", exactString);   // writes 31.00000000000004973799150320701301097869873046875
  Console.WriteLine("General format (G): {0}", evil);               // writes 31.0000000000001
  Console.WriteLine("Round-trip format (R): {0:R}", evil);          // writes 31.00000000000005

  Console.WriteLine();
  Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));

  Console.WriteLine();
  decimal converted = (decimal)evil;
  Console.WriteLine("Decimal version: {0}", converted);             // writes 31
  decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
  Console.WriteLine("Better decimal: {0}", preciseDecimal);         // writes 31.000000000000049737991503207
}

В приведенном выше коде используются SkeetToExactString метод. Если вы не хотите использовать его материалы (можно найти по URL), просто удалите строки кода выше в зависимости отexactString, Вы все еще можете увидеть, какDouble обсуждаемый (evil) округлен и отлит.

ADDITION:

Итак, я проверил еще несколько чисел, и вот таблица:

  exact value (truncated)       "R" format         "G" format     decimal cast
 -------------------------  ------------------  ----------------  ------------
 6.00000000000000'53'29...  6.0000000000000053  6.00000000000001  6
 9.00000000000000'53'29...  9.0000000000000053  9.00000000000001  9
 30.0000000000000'49'73...  30.00000000000005   30.0000000000001  30
 50.0000000000000'49'73...  50.00000000000005   50.0000000000001  50
 200.000000000000'51'15...  200.00000000000051  200.000000000001  200
 500.000000000000'51'15...  500.00000000000051  500.000000000001  500
 1020.00000000000'50'02...  1020.000000000005   1020.00000000001  1020
 2000.00000000000'50'02...  2000.000000000005   2000.00000000001  2000
 3000.00000000000'50'02...  3000.000000000005   3000.00000000001  3000
 9000.00000000000'54'56...  9000.0000000000055  9000.00000000001  9000
 20000.0000000000'50'93...  20000.000000000051  20000.0000000001  20000
 50000.0000000000'50'93...  50000.000000000051  50000.0000000001  50000
 500000.000000000'52'38...  500000.00000000052  500000.000000001  500000
 1020000.00000000'50'05...  1020000.000000005   1020000.00000001  1020000

Первый столбец дает точное (хотя и усеченное) значение, котороеDouble представлять. Второй столбец дает строковое представление из"R" строка формата. Третий столбец дает обычное строковое представление. И, наконец, четвертый столбец даетSystem.Decimal что является результатом преобразования этогоDouble.

Мы заключаем следующее:

Round to 15 digits by ToString() and round to 15 digits by conversion to Decimal disagree in very many cases Conversion to Decimal also rounds incorrectly in many cases, and the errors in these cases cannot be described as "round-twice" errors In my cases, ToString() seems to yield a bigger number than Decimal conversion when they disagree (no matter which of the two rounds correctly)

Я экспериментировал только с такими случаями, как указано выше. Я не проверял, есть ли ошибки округления с номерами других "форм".

 David M18 июн. 2012 г., 16:42
Оно выделено жирным шрифтом посередине ...
 nicholas18 июн. 2012 г., 20:45
Просто включите эту ссылку в мой ответ, но посмотрите на самую нижнюю часть этой страницы, и она описывает точно такое же поведение:msdn.microsoft.com/en-us/library/kfsatb94.aspx (это даже называет двойным округлением.
 Security Hound18 июн. 2012 г., 16:50
Вы спросили, если это ошибка, это не ошибка, они не являются точными значениями.
 flq18 июн. 2012 г., 16:36
Хорошо, это все мило и подробно, но ... каков твой вопрос?
 Jeppe Stig Nielsen18 июн. 2012 г., 16:56
@Ramhound Если вы внимательно прочитаете вопрос, вы увидите, что я полностью осознаю точность. Я знаю, что не каждое число точно представимо. Мой вопрос: это не ошибка вToString() метод? Мы все можем согласиться с тем, что еслиToString() возвращенный"-42.8" на этот номер, это будет ошибка, даже еслиDoubles являются "не точными значениями" (Ваши слова). ТакToString() может иметь ошибку, даже если точность числа с плавающей точкой не является неограниченной.

Ответы на вопрос(4)

эта страница который показывает очень похожее «двойное округление»; проблема.

Проверка двоичного / шестнадцатеричного представления следующих чисел с плавающей запятой показывает, что данный диапазон сохраняется как одно и то же число в двойном формате:

31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e

Как отметили несколько других, это потому, что ближайший представимый двойной тип имеет точное значение 31,00000000000004973799150320701301097869873046875.

Есть два дополнительных аспекта, которые необходимо учитывать при прямом и обратном преобразовании IEEE 754 в строки, особенно в среде .NET.

Во-первых (я не могу найти первоисточник) из Википедии у нас есть:

If a decimal string with at most 15 significant decimal is converted to IEEE 754 double precision and then converted back to the same number of significant decimal, then the final string should match the original; and if an IEEE 754 double precision is converted to a decimal string with at least 17 significant decimal and then converted back to double, then the final number must match the original.

Следовательно, что касается соответствия стандарту, преобразование строки 31.0000000000000497 в double не обязательно будет тем же самым при преобразовании обратно в строку (слишком много знаков после запятой).

Второе соображение состоит в том, что, если преобразование типа double в строку не имеет 17 значащих цифр, поведение округления также явно не определено в стандарте.

Кроме того, документация поDouble.ToString () показывает, что он определяется спецификатором числового формата текущих настроек культуры.

Possible Complete Explanation:

Я подозреваю, что двойное округление происходит примерно так: исходная десятичная строка создается с 16 или 17 значащими цифрами, потому что это требуемая точность для «туда и обратно» конверсия, дающая промежуточный результат 31,00000000000005 или 31,000000000000050. Затем из-за настроек культуры по умолчанию результат округляется до 15 значащих цифр, 31,00000000000001, поскольку 15 десятичных значащих цифр - это минимальная точность для всех двойных чисел.

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

 18 июн. 2012 г., 20:17
(удалил два неконструктивных комментария)
 Jeppe Stig Nielsen18 июн. 2012 г., 17:36
ОК, так что число внутренне присваивается0x403f00000000000e Можно сказать, что (ваша запись) представляет собой весь интервал (длины2 ** -48согласитесь). Ваши примеры доказывают, что этот интервал содержит оба числа, принадлежащие"31" и номера, принадлежащие"31.0000000000001", Такin your opinion это должно быть "неопределенное поведение" какая из этих двух строкToString() выбирает? Вmy opinion он должен использовать среднюю точку этого интервала, а средняя точка принадлежит"31".
 19 июн. 2012 г., 15:42
Числа с плавающей точкой не являются интервалами. Они числа. & quot; строковое представление 31.0000000000001 & quot; являетсяnot & quot; иногда корректно & quot ;, потому что 0x403f00000000000e всегда точно равен 31.00000000000004973799150320701301097869873046875 и никогда больше.
 18 июн. 2012 г., 18:24
Ну, математически и интуитивно, интервальная середина была бы логичным выбором; однако, это, вероятно, будет более вычислительно дорогим. Для определения верхней и нижней границ в десятичном представлении потребуется вдвое больше преобразований. С математической точки зрения нет причин выбирать потолок относительно пола против средней точки. В этом случае средняя точка получает наибольшее значение, поскольку вы априори знаете предполагаемое десятичное представление, но есть равное количество случаев, когда округление в большую сторону обеспечит «правильный» ответ. ответ.
 18 июн. 2012 г., 18:42
@nicholas: доминирующий стандарт IEEE 754 для чисел с плавающей запятой в значительной степени основан на модели, согласно которой отдельные числа с плавающей запятой представляют точные значения, а не диапазоны: «правильные»; Результатом операции является ближайшее число с плавающей запятой к математической операции, вычисленной с этими точными входными значениями. В этом случаеexact значение - это то, что дал OP, и при правильном округлении округление до 15 цифр должно дать31.0не31.0...01, К сожалению, похоже, чтоToString не в состоянии сделать правильное округление здесь.

Да. Увидетьэтот пиар на GitHub, Причиной округления в два раза AFAK является "красивая" Формат вывода, но он вносит ошибку, как вы уже обнаружили здесь. Мы попытались это исправить - убрать прецизионное преобразование из 15 цифр, перейти непосредственно к прецизионному преобразованию из 17 цифр Плохая новость в том, что это серьезное изменение и многое сломает. Например, один из тестовых примеров сломается:

10:12:26 Assert.Equal() Failure 10:12:26 Expected: 1.1 10:12:26 Actual: 1.1000000000000001

Исправление затронет большой набор существующих библиотек, поэтому, наконец, этот PR был закрыт. Однако команда .NET Core все еще ищет шанс исправить эту ошибку. Добро пожаловать в дискуссию.

The culprit is likely the pow operator => **; While your number is exactly representable as a double, for convenience reasons (the power operator needs much work to work right) the power is calculated by the exponential function. This is one reason that you can optimize performance by multiplying a number repeatedly instead of using pow() because pow() is very expensive.

So it does not give you the correct 2^48, but something slightly incorrect and therefore you have your rounding problems. Please check out what 2^48 exactly returns.

РЕДАКТИРОВАТЬ: Извините, я сделал только сканирование по проблеме и дал неправильное подозрение. Есть известная проблема с двойным округлением на процессорах Intel. Старый код использовать внутренний 80-битный формат FPU вместо инструкций SSE, что вероятно вызвать ошибку. Значение записывается точно в 80-битный регистр, а затем закругленныйtwiceДжеппе уже нашел и аккуратно объяснил проблему.

Это ошибка? Ну, процессор все делает правильно, это просто проблема в том, что Intel FPU внутренне имеет больше точности для чисел с плавающей запятой операции.

ДАЛЬНЕЙШЕЕ РЕДАКТИРОВАНИЕ И ИНФОРМАЦИЯ: & Quot; двойное округление & quot; является известной проблемой и явно упоминается в «Справочнике по арифметике с плавающей точкой»; Жан-Мишель Мюллер и др. и др. в главе "Потребность" для ревизии & quot; в разделе «3.3.1 Типичная проблема:« двойное округление »; на странице 75:

The processor being used may offer an internal precision that is wider than the precision of the variables of the program (a typical example is the double-extended format available on Intel Platforms, when the variables of the program are single- precision or double-precision floating-point numbers). This may sometimes have strange side effects , as we will see in this section. Consider the C program [...]

#include <stdio.h>

int main(void) 
{
  double a = 1848874847.0;
  double b = 19954562207.0;
  double c;
  c = a * b;
  printf("c = %20.19e\n", c);
  return 0;
}

32bit: GCC 4.1.2 20061115 в Linux / Debian

С переключателем компилятора или с -mfpmath = 387 (80-битный FPU): 3.6893488147419103232e + 19 -march = pentium4 -mfpmath = sse (SSE) oder 64-bit: 3.6893488147419111424e + 19

Как объясняется в книге, решение для расхождения заключается в двойном округлении с 80 битами и 53 битами.

 19 июн. 2012 г., 17:19
Да, вы правы. Я поправил свой ответ
 Jeppe Stig Nielsen18 июн. 2012 г., 22:22
Я на самом деле не рассчитал2 к48сила Я только что упомянул «математически правильно» дробь для рассматриваемого числа, просто чтобы все знали, что я понимаю, как работает 64-битное число с плавающей точкой. То, как я ввел свой номер в .NET, было через строку кода C #const double evil = 31.0000000000000497;.
 20 июн. 2012 г., 00:32
У вас есть ссылка или более канонический источник для вашего ответа, что это известный выпуск с процессорами Intel?
 18 июн. 2012 г., 22:14
Это не может быть так, потому что он использует тот же решенныйdouble для каждого представления. Просмотрите приведенный пример кода.
Решение Вопроса

Double.ToString не выполняет правильное округление.

Это весьма прискорбно, но не особенно удивительно: выполнение правильного округления для двоичных преобразований в десятичные является нетривиальным, а также потенциально довольно медленным, требующим арифметики с множественной точностью в угловых случаях. Смотри Дэвида Геяdtoa.c кодВот для одного примера того, что вовлечено в правильно округленное двойное к строковому и двойное к строковому преобразованию. (В настоящее время Python использует вариант этого кода для своих преобразований типа float-to-string и string-to-float.)

Даже текущий стандарт IEEE 754 для арифметики с плавающей точкойrecommends, но это не такrequire что преобразования из двоичных типов с плавающей точкой в десятичные строки всегда правильно округляются. Вот фрагмент из раздела 5.12.2 «Последовательности внешних десятичных символов, представляющих конечные числа».

There might be an implementation-defined limit on the number of significant digits that can be converted with correct rounding to and from supported binary formats. That limit, H, shall be such that H ≥ M+3 and it should be that H is unbounded.

ВотM определяется как максимумPmin(bf) по всем поддерживаемым двоичным форматамbf, и с тех порPmin(float64) определяется как17 и .NET поддерживает формат float64 черезDouble тип,M должно быть как минимум17 в сети. Короче говоря, это означает, что если бы .NET следовал стандарту, он обеспечивал бы правильно округленные преобразования строк до не менее 20 значащих цифр. Так выглядит, как будто .NETDouble не соответствует этому стандарту.

В ответ на вопрос «Является ли это ошибкой»? вопрос, так же как и яlike это ошибка, в действительности нигде нет никаких претензий на точность или соответствие IEEE 754, которые я могу найти в документации по форматированию чисел для .NET. Так что это может считаться нежелательным, но мне будет трудно назвать это фактической ошибкой.

РЕДАКТИРОВАТЬ: Джепп Стиг Нильсен указывает, чтоSystem.Double на странице MSDN говорится, что

Double complies with the IEC 60559:1989 (IEEE 754) standard for binary floating-point arithmetic.

Мне не совсем ясно, что именно должно охватывать это заявление о соответствии, но даже для более старой версии IEEE 754 1985 года описанное преобразование строк, по-видимому, нарушает двоичные и десятичные требования этого стандарта.

Учитывая это, я с радостью обновлю свою оценку до «возможной ошибки».

 18 июн. 2012 г., 20:22
Ах, хорошая находка. Это более старая версия IEEE 754 (с 1985 г.), которую они заявляют о соответствии. У меня нет такой копии, но я напоминаю, что требования к преобразованиям с плавающей запятой там были несколько слабее, поэтому вполне возможно, что их версия ToString соответствует этому. Извините, что не могу предложить больше здесь.
 Jeppe Stig Nielsen26 июн. 2012 г., 18:07
Я приму этот ответ. Обратите внимание, что я только что отредактировал свой вопрос, чтобы предоставить больше примеров. Преобразование вDecimal раунды тоже неправильно!
 Jeppe Stig Nielsen18 июн. 2012 г., 19:49
Очень информативно. На сайте MSDN они упоминают IEEE наSystem.Double page (& quot; Double соответствует стандарту IEC 60559: 1989 (IEEE 754) для двоичной арифметики с плавающей точкой. & quot;) иC# double page, поэтому можно подумать, что это было их намерение соблюдать IEEE сToString также.
 18 июн. 2012 г., 19:43
+1 за хороший предварительный ответ ... к тому моменту, когда вы заметите, что это не ошибка ... Я согласен, но в той степени, в которой документация MSDN вводит в заблуждение относительно поведения (не будет в первый раз). ... Я думаю, что это подкрепляет то, что мы уже знаем: если вы хотите, чтобы во время математических операций сохранялась значимость и точность в 15 цифр (для OP), то ниdouble ниfloat являются подходящими типами.
 18 июн. 2012 г., 20:28
@Jeppe Стиг Нильсен: Только что нашел копию версии 1985 года; даже там, если я правильно его читаю, они указывают диапазон, в пределах которого двоичные в десятичные (то есть строки) преобразования должны быть правильно округлены, и ваш пример попадает в этот диапазон.

Ваш ответ на вопрос