Конвертировать float в double теряет точность, но не через ToString - PullRequest
22 голосов
/ 13 мая 2011

У меня есть следующий код:

float f = 0.3f;
double d1 = System.Convert.ToDouble(f);
double d2 = System.Convert.ToDouble(f.ToString());

Результаты эквивалентны:

d1 = 0.30000001192092896;
d2 = 0.3;

Мне интересно узнать, почему это так?

Ответы [ 3 ]

23 голосов
/ 13 мая 2011

Это не потеря точности .3 не является представимым в плавающей точке . Когда система преобразуется в строку, она округляется; если вы напечатаете достаточно значащие цифры, вы получите что-то более понятное.

Чтобы увидеть это более четко

float f = 0.3f;
double d1 = System.Convert.ToDouble(f);
double d2 = System.Convert.ToDouble(f.ToString("G20"));

string s = string.Format("d1 : {0} ; d2 : {1} ", d1, d2);

вывод

"d1 : 0.300000011920929 ; d2 : 0.300000012 "
14 голосов
/ 13 мая 2011

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

Что здесь происходит, так это то, что у вас есть 32 бита, выделенных для вашего float. Затем вы увеличиваете значение до двойного, добавляя еще 32 бита для представления своего номера (всего 64). Эти новые биты являются наименее значимыми (самые дальние справа от вашей десятичной запятой) и не имеют никакого отношения к фактическому значению, поскольку они были неопределенными ранее. В результате эти новые биты имеют те значения, которые они имели, когда вы делали апскейд. Они так же неопределенны, как и раньше - другими словами, мусор.

Когда вы понижаете число с двойного до плавающего, оно отбрасывает эти младшие значащие биты, оставляя вас с 0,300000 (7 цифр точности).

Механизм преобразования строки в число с плавающей запятой отличается; компилятору необходимо проанализировать семантическое значение строки символов «0.3f» и выяснить, как это относится к значению с плавающей запятой. Это невозможно сделать с помощью сдвига битов, например, преобразования с плавающей запятой / двойного, то есть ожидаемого значения.

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

редактирование:

Во-первых, как указал @phoog ниже, преобразование из числа с плавающей точкой в ​​двойное не так просто, как добавление еще 32 бит в пространство, зарезервированное для записи числа. В действительности вы получите дополнительно 3 бита для показателя степени (всего 11) и дополнительные 29 бит для дроби (всего 52). Добавьте бит знака, и вы получите 64 бита для двойного.

Кроме того, предположить, что в этих наименее значимых местах есть «мусорные биты», что является грубым обобщением и, вероятно, не подходит для C #. Небольшое объяснение и некоторое тестирование, приведенное ниже, позволяют мне предположить, что это является детерминированным для C # /. NET и, вероятно, является результатом какого-то конкретного механизма преобразования, а не резервирования памяти для дополнительной точности.

В прежние времена, когда ваш код компилировался в двоичный файл машинного языка, компиляторы (по крайней мере, компиляторы C и C ++) не добавляли никаких инструкций ЦП для «очистки» или инициализации значения в памяти, когда вы зарезервировали пространство для переменной. Таким образом, если программист явно не инициализирует переменную каким-либо значением, значения битов, которые были зарезервированы для этого расположения, сохранят любое значение, которое они имели до того, как вы зарезервировали эту память.

В .NET land ваш C # или другой язык .NET компилируется в промежуточный язык (CIL, Common Intermediate Language), который затем Just-In-Time компилируется CLR для выполнения в качестве собственного кода. Может быть или не быть шаг инициализации переменной, добавленный либо компилятором C #, либо компилятором JIT; Я не уверен.

Вот что я знаю:

  • Я проверил это, применив число с плавающей точкой к трем различным двойникам. Каждый из результатов имел одинаковое значение.
  • Это значение было точно таким же, как и значение @ rerun, приведенное выше: double d1 = System.Convert.ToDouble(f); результат: d1 : 0.300000011920929
  • Я получаю тот же результат, если я использую double d2 = (double)f; Результат: d2 : 0.300000011920929

С учетом того, что трое из нас получают одинаковые значения, похоже, что значение upcast является детерминированным (а не фактически мусорными битами), что указывает на то, что .NET выполняет что-то одинаково на всех наших машинах.Все еще верно сказать, что дополнительные цифры не более или менее точны, чем они были раньше, потому что 0,3f не совсем равен 0,3 - он равен 0,3, с точностью до семи цифр.Мы ничего не знаем о значениях дополнительных цифр, кроме тех первых семи.

3 голосов
/ 21 сентября 2015

Я использую десятичное приведение для правильного результата в этом случае и в том же другом случае

float ff = 99.95f;
double dd = (double)(decimal)ff;
...