проблема с плавающей запятой

#c

#c

Вопрос:

У меня есть плавающее значение, равное 0.1, вводимое из пользовательского интерфейса.

Но при преобразовании этой строки в float я получаю значение 0.10 … 01. Проблема заключается в добавлении ненулевой цифры. Как мне решить эту проблему.

Спасибо,

iSight

Ответ №1:

Вам нужно выполнить некоторое ознакомительное чтение с представлениями с плавающей запятой: http://docs.sun.com/source/806-3568/ncg_goldberg.html.

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

Ваши варианты заключаются в том, чтобы:

  • отобразите его обратно с меньшим количеством цифр, чтобы округлить обратно до базового значения 10 (извлеките <iomanip> заголовок стандартной библиотеки и установите точность)
  • сохраните число в каком-нибудь реальном объекте с поддержкой десятичной системы счисления — вы найдете множество классов C для этого в Google, но ни один из них не предусмотрен ни в стандарте, ни в boost, который я просматривал в последний раз
  • преобразуйте входные данные из строки непосредственно в целое число, состоящее из некоторой меньшей единицы (например, тысячных долей), избегая округления.

Ответ №2:

0,1 (десятичный) = 0,00011001100110011 … (двоичный)

Итак, в общем случае число, которое вы можете представить конечным числом десятичных разрядов, может быть невозможно представить конечным числом битов. Но числа с плавающей запятой хранят только самые N значащих битов. Итак, преобразования между десятичной строкой и «двоичным» значением с плавающей запятой обычно включают округление.

Однако преобразование десятичной строки в обратном направлении без потерь -> двойная -> десятичная строка возможно, если вы ограничиваетесь десятичными строками, содержащими не более 15 значащих цифр (при условии, что IEEE 754 содержит 64-разрядные числа с плавающей запятой). Это включает в себя последнее преобразование. Вам нужно создать строку из double, в которой не более 15 значащих цифр.

Также возможно выполнить обратный переход double -> string -> double без потерь. Но здесь вам могут понадобиться десятичные строки с 17 десятичными цифрами, чтобы заставить его работать (опять же, при условии, что IEEE-754 использует 64-битные числа с плавающей запятой).

Ответ №3:

Лучший сайт, который я когда-либо видел, который объясняет, почему некоторые числа не могут быть представлены точно, — это сайт-конвертер IEEE754 Харальда Шмидта.

Это онлайн-инструмент для отображения представлений значений одинарной точности IEEE754, и мне это так понравилось, что я написал свое собственное Java-приложение для этого (а также с двойной точностью).

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

Если вам нужна большая точность и / или лучший тип для десятичных значений, вы можете либо:

  • переключитесь на большее количество битов.
  • используйте десятичный тип
  • используйте библиотеку с большими числами, такую как GMP (хотя я отказываюсь использовать это в производственном коде, поскольку обнаружил, что она не справляется с нехваткой памяти элегантно).

В качестве альтернативы вы можете использовать неточные значения (частота ошибок в них очень низкая, что-то вроде одной части на сто миллионов для чисел с плавающей запятой, из памяти) и просто распечатать их с меньшей точностью. Распечатка 0.10000000145 с точностью до двух знаков после запятой поможет вам 0.10 .

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


Что касается того, почему вы получаете это значение, 0.1 оно хранится в формате одинарной точности IEEE754 следующим образом (знак, экспонента и мантисса):

 s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm    1/n
0 01111011 10011001100110011001101
           |||||||||||||||||||||| - 8388608
           ||||||||||||||||||||| -- 4194304
           |||||||||||||||||||| --- 2097152
           ||||||||||||||||||| ---- 1048576
           |||||||||||||||||| -----  524288
           ||||||||||||||||| ------  262144
           |||||||||||||||| -------  131072
           ||||||||||||||| --------   65536
           |||||||||||||| ---------   32768
           ||||||||||||| ----------   16384
           |||||||||||| -----------    8192
           ||||||||||| ------------    4096
           |||||||||| -------------    2048
           ||||||||| --------------    1024
           |||||||| ---------------     512
           ||||||| ----------------     256
           |||||| -----------------     128
           ||||| ------------------      64
           |||| -------------------      32
           ||| --------------------      16
           || ---------------------       8
           | ----------------------       4
            -----------------------       2
  

Знак положительный, это довольно просто.

Показатель степени равен 64 32 16 8 2 1 = 123 - 127 bias = -4 , поэтому множитель равен 2-4 или 1/16 .

Мантисса слишком объемная. Он состоит из 1 (неявной базы) плюс (для всех этих битов, каждый из которых равен 1 / (2n), поскольку n начинается с 1 и увеличивается вправо), {1/2, 1/16, 1/32, 1/256, 1/512, 1/4096, 1/8192, 1/65536, 1/131072, 1/1048576, 1/2097152, 1/8388608} .

Когда вы складываете все это, вы получаете 1.60000002384185791015625 .

Когда вы умножаете это на множитель, вы получаете 0.100000001490116119384765625 , совпадающее со значением двойной точности на сайте Харальда, насколько оно напечатано:

 0.10000000149011612 (out by 0.00000000149011612)
  

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

 0.09999999403953552 (out by 0.00000000596046448)
  

Объединение этих двух:

 0.10000000149011612 (out by 0.00000000149011612)
                                      |
0.09999999403953552 (out by 0.00000000596046448)
  

вы можете видеть, что первый вариант соответствует примерно в четыре раза (14.9: 59.6). Итак, это самое близкое значение, которое вы можете получить 0.1 .

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

1. Однажды я отлаживал встроенную систему, которая работала непрерывно, считывая показания датчика и интегрируя результаты измерений, чтобы накапливать общее количество примерно раз в секунду. Первоначальный автор (не я) сохранил накопитель с одинарной точностью float . Когда процесс выполняется непрерывно в течение месяцев или лет, неудивительно, что он достигает точки, когда накопитель был достаточно большим по величине, чем типичное инкрементное изменение, чтобы заставить его больше не отслеживать истинное значение.

Ответ №4:

Поскольку числа с плавающей запятой хранятся в двоичном формате, дробная часть фактически находится в базе два … и одна десятая является повторяющимся десятичным числом в базе два, таким же, как одна девятая в базе десять.

Наиболее распространенные способы решения этой проблемы — хранить ваши значения в виде целых чисел соответствующего масштаба, как в типах валюты C # или SQL, или округлять числа с плавающей запятой при их отображении.