2010-05-27 4 views
9

Итак, я пытаюсь выяснить, почему оператор modulo возвращает такое большое необычное значение.Арифметика с плавающей точкой - Оператор Modulo по двойному типу

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

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 в следующих должностях onetwothree, среди прочих.

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

Редактировать: Я должен указать, что я знаю, что 0,1 является infinitely repeating series of numbers in binary - это имеет к этому какое-либо отношение?

ответ

14

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

с 0 = 0,1 mod 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 и, таким образом, получит .0999999999999999500399638989679556809365749359130859375 в качестве окончательного ответа.

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

+0

0.1000000000000000055511151231257827021181583404541015625, а точнее. – dan04

+0

Я ценю прогулку по расчету. Одно уточнение - вы упоминаете «с 0 = 0,1 mod 0,1, фактическая ошибка в мод составляет 0,1 - 0,09999999 ... - очень мало». Я смущен этим, потому что мой результат вернулся 0.099999, но фактический результат равен 0. Как фактическая ошибка 0.1 - 0.099999? Вы можете немного рассказать об ошибке в этой части? – CrimsonX

+0

, чтобы ответить на предыдущий комментарий для будущих читателей: раздел «Редактировать» отвечает на него. (короткое объяснение: ошибка велика из-за нестабильности мод (функция вмятины пилы) небольшое увеличение 'a' (например, 10,0001) приведет к падению результата до около 0) –

2

Это не совсем «ошибка» в вычислении, а то, что у вас никогда не было 0,1, чтобы начать с.

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

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

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

+0

Но почему бы 1,0% 0,1 показать эту ошибку и 1,0/0,1 не показывает ошибку? – CrimsonX

+0

Также я думаю, что вы имеете в виду «машина оставлена ​​для вычисления 1.0% 0.0999 ...» – CrimsonX

+0

Причина 1.0/0.1 выходит «правильно», потому что 1.0/.0999 ... и 1.0/0.1 почти равны - функция непрерывна. Расчет выполняется с несколькими дополнительными битами, конечный представляемый результат в обоих случаях одинаковый. Затем он выглядит «правильно», основываясь на ожидании ответа на 1.0/exact_0.1 на 10. Но помните, что проблема, представленная машине, не была 1.0% 0.1, а 1.0% 0.10000000001 или что-то, и этот расчет действительно приводит к большой остаток, который нельзя округлить. – DigitalRoss

2

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

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

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

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

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

0

Ошибка, которую вы видите, невелика; он выглядит только на первый взгляд. Ваш результат (после округления для отображения) был 0.09999999999999995 == (0.1 - 5e-17), когда вы ожидали 0 от % 0.1 операция. Но помните, что это почти 0,1 и 0.1 % 0.1 == 0.

Так что ваша фактическая ошибка здесь -5e-17. Я бы назвал это маленьким.

В зависимости от того, что вам нужен номер для, было бы лучше написать:

double result = 1.0 % 0.1; result = result >= 0.1/2 ? result - 0.1 : result;

Смежные вопросы