Преобразование float в double теряет точность, но не через toString

#c# #string #floating-point #double #precision

#c# #строка #с плавающей запятой #double #точность

Вопрос:

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

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

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

 d1 = 0.30000001192092896;
d2 = 0.3;
  

Мне любопытно узнать, почему это?

Комментарии:

1. Возможно, вы найдете это руководство с плавающей запятой полезным.

Ответ №1:

Это не потеря точности . 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 "
  

Комментарии:

1. Ага, это имеет смысл, поэтому метод toString по умолчанию просто усекает выходные данные, округляя (и технически делает его еще менее точным). Но округление позволяет мне получить начальное значение, которое я установил.

2. 1! Два вопроса… До какого значения округляется значение float (сколько цифр) при преобразовании в строку? И, что более важно, ПОЧЕМУ? Если кто-то использует float и присваивает значение, но это точное значение не сохраняется из-за ограничений float, с какой стати toString решит округлить для вас? Это еще хуже, потому что выходные данные отладчика, конечно, делают то же самое, поэтому что-то вроде (float) 0.3 по-прежнему отображает 0.3 в выходных данных отладки, и вы никогда не осознаете, что теряете эту точность. Это глупо.

3. именно так работают все с плавающей запятой. Для представления бесконечно большого числа действительных чисел требуется не так много битов. Ошибка epsilon устраняется, и логика для отображения знает, что когда значение epsilon около .3 достаточно низкое, чтобы это было видно .3. Подробности по ссылке

Ответ №2:

Вы не теряете точность; вы повышаете точность до более точного представления (double, длиной 64 бита) из менее точного представления (float, длиной 32 бита). То, что вы получаете в более точном представлении (после определенной точки), — это просто мусор. Если бы вы преобразовали его обратно в float ИЗ double, у вас была бы точно такая же точность, как и раньше.

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

Когда вы понижаете значение от double до float, это приведет к удалению этих наименее значимых битов, оставляя вам 0.300000 (7 цифр точности).

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

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

Редактировать:

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

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

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

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

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

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

Поскольку трое из нас получают одинаковые значения, похоже, что приведенное значение является детерминированным (и на самом деле не является мусорными битами), что указывает на это.NET делает что-то же самое на всех наших машинах. По-прежнему верно сказать, что дополнительные цифры не более и не менее точны, чем были раньше, потому что 0.3f не совсем равно 0.3 — оно равно 0.3, с точностью до семи цифр. Мы ничего не знаем о значениях дополнительных цифр, кроме этих первых семи.

Комментарии:

1. Спасибо Джо, здесь есть отличная информация, я понял преобразование float против double в первой строке, основная проблема заключалась в понимании того, что произошло во 2-й строке для достижения результата, который я искал. Спасибо!

2. Этот бит о том, что младшие значащие биты являются тем мусором, который мог быть в памяти раньше, неверен (по крайней мере, не в C #). Во-первых, float — это не просто double с удаленными 32 битами; количество битов, используемых для указания показателя степени, отличается, как и смещение показателя степени. Во-вторых, если бы это было правдой, было бы невозможно последовательно переходить от float к double и обратно.

3. Вы правы, говоря, что это не так просто, как добавить дополнительные 32 бита; Я изменю свой ответ, чтобы отразить это. Я не уверен насчет мусорных битов в C #, хотя; пока . NET будет работать с CLR, а не изначально, я недостаточно знаю о том, как работает CLR, чтобы знать, будет ли она очищать / обнулять 29 младших значащих битов, когда вы выполняете подобную передачу. У вас есть какие-либо ресурсы, которые вы могли бы порекомендовать?

Ответ №3:

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

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

Комментарии:

1. Внимание: это может вызвать исключение OverflowException!

2. Это, вероятно, намного более производительно, чем решение toString ()! Диапазон -10 ^ 28 меня устраивает.

3. -7.922816E27 более безопасен.