Арифметика с плавающей точкой - оператор по модулю на двойном типе

Таким образом, я пытаюсь выяснить, почему оператор по модулю возвращает такое большое необычное значение.

Если у меня есть код:

double result = 1.0d % 0.1d;

это даст результат 0.09999999999999995. Я ожидал бы значение 0

Обратите внимание, что эта проблема не существует с помощью делящегося оператора - double result = 1.0d / 0.1d;

даст результат 10.0, подразумевать, что остаток должен быть 0.

Позвольте мне быть ясным: я не удивлен, что ошибка существует, я удивлен, что ошибка так штопка, большая по сравнению с числами в действии. 0,0999 ~ = 0.1 и 0.1 находятся на том же порядке величины как 0.1d и только один порядок величины далеко от 1.0d. Не как Вы может сравнить его с double.epsilon или сказать "его равное если его <0,00001 различия".

Я читал по этой теме на StackOverflow, в следующем один два три сообщений, среди других.

Может кто-либо предлагать объяснить, почему эта ошибка является настолько большой? Любые любые предложения, чтобы не сталкиваться с проблемами в будущем (я знаю, что мог использовать десятичное число вместо этого, но я обеспокоен производительностью этого).

Править: Я должен конкретно указать, что знаю, что 0.1 бесконечно повторяющаяся серия чисел в двоичном файле - который имеет какое-либо отношение к нему?

9
задан Community 23 May 2017 в 11:48
поделиться

3 ответа

Ошибка возникает из-за того, что число типа double не может точно представлять 0,1 - самое близкое значение, которое оно может представлять, - это примерно 0,100000000000000005551115123126. Теперь, когда вы делите 1,0 на это, вы получаете число чуть меньше 10, но опять же, двойное число не может точно его представить, поэтому оно округляется до 10. Но когда вы выполняете мод, он может дать вам это немного. менее 0,1 остатка.

поскольку 0 = 0,1 по модулю 0,1, фактическая ошибка модуля составляет 0,1 - 0,09999999 ... - очень мала.

Если вы добавите результат оператора% к 9 * 0,1, он снова даст вам 1,0.

Править

Немного подробнее об округлении - особенно потому, что эта проблема является хорошим примером опасностей смешанной точности.

Обычно вычисляется a% b для чисел с плавающей запятой как a - (b * floor (a / b)) . Проблема в том, что это может быть выполнено все сразу с большей внутренней точностью, чем вы получили бы с этими операциями (и округлением результата до числа fp на каждом этапе), поэтому это может дать вам другой результат. Один из примеров, который видят многие, - это аппаратное обеспечение Intel x86 / x87, использующее 80-битную точность для промежуточных вычислений и только 64-битную точность для значений в памяти. Таким образом, значение в b в приведенном выше уравнении поступает из памяти и, таким образом, является 64-битным числом fp, которое не совсем 0,1 (спасибо dan04 за точное значение), поэтому, когда он вычисляет 1,0 / 0,1, он получает 9.99999999999999944488848768742172978818416595458984375 (округлено до 80 бит). Теперь, если вы округлите это до 64 бит, получится 10.0, но если вы сохраните 80-битный внутренний и сделаете на нем пол, он усечется до 9.0 и, таким образом, получит .0999999999999999500399638918679556809365749359130859375 в качестве окончательного ответа.

Итак, в этом случае вы видите большую очевидную ошибку, потому что вы используете прерывистую ступенчатую функцию (пол), что означает, что очень крошечная разница во внутреннем значении может подтолкнуть вас к шагу. Но так как mod сам по себе является прерывистой ступенчатой ​​функцией, этого следовало ожидать, и реальная ошибка здесь составляет 0,1-0,0999 ... поскольку 0,1 - это точка разрыва в диапазоне функции mod.

14
ответ дан 4 December 2019 в 11:40
поделиться

Тот факт, что 0,1 не может быть точно представлен в двоичном формате, напрямую связан с этим.

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

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

Также обратите внимание, что / - в основном непрерывная функция (небольшая разница в аргументах обычно означает небольшую разницу в результате, и хотя производная может быть большой около нуля, но с той же стороны, по крайней мере, дополнительная точность для аргументы помогают). С другой стороны,% не является непрерывным: какую бы точность вы ни выбрали, всегда будут аргументы, для которых произвольно малая ошибка представления первого аргумента означает большую ошибку результата.

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

2
ответ дан 4 December 2019 в 11:40
поделиться

Это не совсем "ошибка" в вычислениях, а тот факт, что у вас никогда не было 0.1 для начала.

Проблема в том, что 1.0 можно точно представить в двоичной системе с плавающей запятой, а 0.1 - нет, потому что его нельзя точно построить из отрицательных степеней двойки. (Это 1/16 + 1/32 + ...)

Таким образом, вы не получаете 1.0 % 0.1, машине остается вычислить 1.0 % 0.1 +- 0.00... и затем она честно сообщает, что она получила в результате...

Чтобы получить большой остаток, я полагаю, что второй операнд % должен быть немного больше 0.1, что препятствует окончательному делению и приводит к тому, что почти все 0.1 являются результатом операции.

2
ответ дан 4 December 2019 в 11:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: