Ошибка Round-дважды в методе .NET Double.ToString
Математически рассмотрим для этого вопроса рациональное число
8725724278030350 / 2**48
где **
в знаменателе обозначает возведение в степень, т.е. знаменатель 2
до 48
-й степени. (Дробь не находится в младших членах, сводится к 2.) Это число точно представляется как System.Double
. Его десятичное разложение есть
31.0000000000000'49'73799150320701301097869873046875 (exact)
где апострофы не представляют отсутствующие цифры, а просто отмечают суки, где округление до 15 соответственно. 17.
Обратите внимание на следующее: если это число округлено до 15 цифр, результат будет 31
(за ним следует тринадцать 0
s), потому что следующие цифры (49...
) начинаются с 4
(что означает round вниз). Но если число сначала округлено до 17 цифр, а затем округлено до 15 цифр, результат может быть 31.0000000000001
. Это связано с тем, что первое округление округляется, увеличивая цифры 49...
до 50 (terminates)
(следующие цифры были 73...
), а второе округление затем снова округлялось (когда правило округления средней точки гласит: "округлить от нуля" ).
(Разумеется, есть много других чисел с вышеуказанными характеристиками.)
Теперь, оказывается, стандартное строковое представление .NET этого числа "31.0000000000001"
. Вопрос: не является ли это ошибкой?. По стандартным строковым представлениям мы подразумеваем String
, созданный методом экземпляров параметров Double.ToString()
, который, конечно, идентичен тому, что создается ToString("G")
.
Интересно отметить, что если вы набросаете вышеприведенный номер на System.Decimal
, тогда вы получите точно decimal
31
! См. этот вопрос о переполнении стека для обсуждения удивительного факта, что отбрасывание Double
в decimal
включает в себя первое округление до 15 цифр. Это означает, что кастинг на decimal
делает правильный раунд до 15 цифр, тогда как вызов ToSting()
делает неправильный.
Подводя итог, мы имеем число с плавающей запятой, которое при выводе на пользователя 31.0000000000001
, но при преобразовании в decimal
(где 29 цифры доступны), становится 31
точно. Это печально.
Вот несколько С# -кодов для проверки проблемы:
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
}
В приведенном выше коде используется метод Skeet ToExactString
. Если вы не хотите использовать его материал (может быть найден через URL-адрес), просто удалите строки кода, зависящие от exactString
. Вы все еще можете увидеть, как рассматриваемый Double
(evil
) закруглен и сбрасывается.
Сложение:
ОК, поэтому я проверил еще несколько номеров, и вот таблица:
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
.
Мы заключаем следующее:
- Круглые до 15 цифр
ToString()
и округлые до 15 цифр путем преобразования в decimal
не согласуются во многих случаях
- Преобразование в
decimal
также во многих случаях неверно округляется, и ошибки в этих случаях не могут быть описаны как ошибки "round-two"
- В моих случаях
ToString()
, кажется, дает большее число, чем decimal
, когда они не согласны (независимо от того, какой из двух раундов правильно)
Я только экспериментировал с такими случаями, как выше. Я не проверял, есть ли ошибки округления с числами других "форм".
Ответы
Ответ 1
Итак, из ваших экспериментов кажется, что Double.ToString
не делает правильного округления.
Это довольно неудачно, но не особенно удивительно: правильное округление двоичных к десятичным преобразованиям является нетривиальным, а также потенциально довольно медленным, требующим многоточечной арифметики в угловых случаях. См. David Gay dtoa.c
code здесь для одного примера того, что связано с правильно округленным преобразованием double-to-string и string-to-double. (Python в настоящее время использует вариант этого кода для своих преобразований float-to-string и string-to-float.)
Даже текущий стандарт IEEE 754 для арифметики с плавающей запятой рекомендует, но не требует, чтобы преобразования из двоичных типов с плавающей запятой в десятичные строки всегда были правильно округлены. Здесь приведен фрагмент из раздела 5.12.2 "Внешние десятичные последовательности символов, представляющие конечные числа".
Может существовать предел, определенный реализацией, по числу значимые цифры, которые могут быть преобразованы с правильным округлением в и из поддерживаемых двоичных форматов. Этот предел H должен быть таким, что H ≥ M + 3, и должно быть, что H неограничен.
Здесь M
определяется как максимум Pmin(bf)
по всем поддерживаемым двоичным форматам bf
, а так как Pmin(float64)
определяется как 17
, а .NET поддерживает формат float64 с помощью типа Double
M
должен быть не менее 17
в .NET. Короче говоря, это означает, что если .NET будет следовать стандарту, он будет обеспечивать корректное округленное преобразование строк, по меньшей мере, до 20 значащих цифр. Таким образом, похоже, что .NET Double
не соответствует этому стандарту.
В ответ на вопрос "Это ошибка", так как я бы хотел, чтобы это была ошибка, действительно нет никаких претензий относительно точности или соответствия IEEE 754 в любом месте, которое я могу найти в документация формата форматирования для .NET. Таким образом, это может считаться нежелательным, но мне было бы трудно назвать его реальной ошибкой.
EDIT: Jeppe Stig Nielsen указывает, что страница System.Double в MSDN заявляет, что
Двойной соответствует стандарту IEC 60559: 1989 (IEEE 754) для двоичных арифметика с плавающей запятой.
Мне не ясно, что именно этот оператор соответствия должен охватывать, но даже для более старой версии IEEE 754 в 1985 году описанное преобразование строк нарушает требования двоичного к десятичному значению этого стандарта.
Учитывая это, я с радостью повышу свою оценку до "возможной ошибки".
Ответ 2
Сначала взгляните на нижнюю часть на этой странице, которая показывает очень похожую проблему с двойным округлением.
Проверка двоичного/шестнадцатеричного представления следующих чисел с плавающей запятой показывает, что данный диапазон сохраняется как один и тот же номер в двойном формате:
31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e
Как отмечалось несколькими другими, это связано с тем, что ближайший представимый двойник имеет точное значение 31.00000000000004973799150320701301097869873046875.
Есть два дополнительных аспекта, которые следует учитывать при прямом и обратном преобразовании IEEE 754 в строки, особенно в среде .NET.
Сначала (я не могу найти первичный источник) из Википедии мы имеем:
Если десятичная строка, содержащая не более 15 значащих десятичных знаков, преобразуется к двойной точности IEEE 754, а затем преобразован обратно в тот же число значащих десятичных чисел, то окончательная строка должна соответствовать оригинал; и если двойная точность IEEE 754 преобразуется в десятичная строка с не менее чем 17 значащими десятичными знаками, а затем преобразованная назад, чтобы удвоить, то окончательное число должно соответствовать оригиналу.
Поэтому, в отношении соответствия стандарту, преобразование строки 31.0000000000000497 в double не обязательно будет одинаковым при преобразовании обратно в строку (слишком много десятичных знаков).
Второе соображение состоит в том, что, если преобразование с двойным строком не имеет 17 значащих цифр, это поведение округления также явно не определено в стандарте.
Кроме того, документация по Double.ToString() показывает, что она определяется спецификатором числового формата текущих настроек культуры.
Возможное полное объяснение:
Я подозреваю, что двойное округление происходит примерно так: начальная десятичная строка создается на 16 или 17 значащих цифр, потому что это требуемая точность для конверсии "туда и обратно", дающая промежуточный результат 31.00000000000005 или 31.000000000000050. Затем из-за настроек культуры по умолчанию результат округляется до 15 значащих цифр, 31.00000000000001, поскольку 15 значащих десятичных цифр являются минимальной точностью для всех удвоений.
Выполняя промежуточное преобразование в Decimal, с другой стороны, избегает этой проблемы по-другому: напрямую сокращает до 15 значащих цифр.
Ответ 3
Вопрос: Разве это не ошибка?
Да. См. этот PR на 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 по-прежнему ищет шанс исправить эту ошибку. Добро пожаловать, чтобы присоединиться к обсуждению.
Ответ 4
У меня более простое подозрение: виновником, скорее всего, является оператор pow = > **;
Хотя ваш номер точно представлен как двойной, для удобства
(силовому оператору требуется много работы для правильной работы), мощность рассчитывается
по экспоненциальной функции. Это одна из причин, по которой вы можете оптимизировать производительность
умножая число раз вместо использования pow(), потому что pow() очень
дорогой.
Таким образом, он не дает вам правильного 2 ^ 48, но что-то немного неправильное и
поэтому у вас проблемы с округлением.
Проверьте, что именно возвращается 2 ^ 48.
Забастовкa >
EDIT: Извините, я просмотрел только проблему и дал неправильное подозрение. Там есть
известная проблема с двойным округлением на процессорах Intel. Более старый код использует
внутренний 80-битный формат FPU вместо инструкций SSE, который, вероятно,
чтобы вызвать ошибку. Значение записывается точно в регистр 80 бит, а затем
округленный дважды, поэтому Джеппе уже нашел и аккуратно объяснил проблему.
Это ошибка? Ну, процессор делает все правильно, это просто
проблема в том, что Intel FPU внутренне имеет более высокую точность для плавающих точек
операции.
ДАЛЬНЕЙШАЯ РЕДАКТИРОВКА И ИНФОРМАЦИЯ:
"Двойное округление" является известной проблемой и явно упоминается в "Справочнике по арифметике с плавающей точкой" Жан-Мишеля Мюллера и др. и др. в главе "Необходимость
для пересмотра "под" 3.3.1 Типичная проблема: "двойное округление" на стр. 75:
Используемый процессор может предложить внутреннюю точность, которая шире чем точность переменных программы (типичный пример это формат с двойным расширением, доступный на платформах Intel, когда переменные программы являются одноточечными или с двойной точностью числа с плавающей запятой). Иногда это может иметь странные побочные эффекты, поскольку мы увидим в этом разделе. Рассмотрим программу C [...]
#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
С Compilerswitch или с -mfpmath = 387 (80 бит-FPU): 3.6893488147419103232e + 19
-march = pentium4 -mfpmath = sse (SSE) или 64-бит: 3.6893488147419111424e + 19
Как объясняется в книге, решение для несоответствия - это двойное округление с 80 бит и 53 бит.