Арифметическое c сравнение, чтобы избежать ошибок с плавающей запятой - PullRequest
0 голосов
/ 24 февраля 2020

У меня есть следующая проверка в одной из моих Java функций ...

return floor(value * 100.) % 5 == 0;

Мне сказали, что существует вероятность ошибок с плавающей запятой, потому что я сравниваю необработанные значения, используя == но я не понимаю почему. Я думал, что использование floor позволит избежать этого. Почему может быть ошибка с плавающей запятой и каков наилучший способ в этом случае избегать таких ошибок?

Ответы [ 3 ]

4 голосов
/ 24 февраля 2020

Почему может быть ошибка с плавающей запятой ...

Существует три возможных места для ошибки с плавающей запятой в отображаемом выражении:

  • value предположительно является результатом предыдущих вычислений, и в них могли быть ошибки. Мы не располагаем информацией о них, поскольку вы их не показывали.
  • Код предполагает, что есть некоторый интерес к value как некоторому числу сотых: .01, .02, .03 и т. Д. За исключением .00, .25, .50 и .75, никакие сотые не представимы в двоичном формате с плавающей запятой, используемом Java. Таким образом, даже если предыдущие вычисления были выполнены с максимально возможной точностью, конечный результат не может быть равен любому сотому, кроме .00, .25, .50 или .75. Они будут хотя бы немного выше или ниже. Если они ниже, то умножение на 100 и взятие floor может привести к значению на единицу меньше, чем вы хотите.
  • Когда value умножается на 100, если результат не совсем точно представлен (что может не быть, потому что value может иметь 53 значащих бита, а 100 имеет 5 значащих битов, поэтому продукт может иметь 58 значащих битов, но только 53 могут быть представлены в формате Java double), тогда он округляется до ближайшего представимого значение.

Арифметика с плавающей точкой c в значительной степени рассчитана на приблизительную действительную арифметику c. Для этой цели обычно хорошо использовать непрерывные функции, такие как умножение, синус и т. Д. Часто с непрерывными функциями небольшие ошибки в вычислениях приводят к небольшим ошибкам в результатах. Однако floor, % и == являются прерывистыми функциями: в них есть места, где некоторые изменения на входах вызывают скачки на выходах. Для пола небольшое изменение с 3,99 до 4 вызывает скачок 1 на выходе. Для % небольшое изменение с 3.99 % 4 до 4 % 4 вызывает скачок почти 4. Для == небольшое изменение с 3.99 == 4 до 4 == 4 вызывает скачок с false до true .

… какова лучшая практика в этом случае, чтобы избежать таких ошибок?

В целом, лучшая практика, когда вам нужны точные результаты для вычислений, включающих % использовать целочисленное арифметическое c или использовать с плавающей запятой осторожно, чтобы гарантировать точность всех операций (как при выполнении целочисленного арифметического c в пределах, где с плавающей запятой представлены все целые числа, −2 53 * От 1042 * до + 2 53 для Java double). Если вам нужно обойти это, то нет единой наилучшей практики; хорошие решения будут зависеть от того, что вы делаете для получения value и каких результатов вы пытаетесь достичь.

1 голос
/ 24 февраля 2020

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

Все сводится к тому, как эти значения хранятся в памяти - если я не ошибаюсь, в Java, float с и double с хранятся в стандартном формате IEEE 754.

В любом случае, вместо использования оператора равенства (==), мы должны полагаться на реляционные операторы: меньше (<) или больше (>) для сравнения значений float и double, потому что, в конце, числа с плавающей точкой являются лишь приблизительными , они не являются точными.

Другой подход (мне лично нравится) заключается в использовании Float.compare / Double.compare, если вы более знакомы с семантикой compareTo -вид сравнений.

В качестве альтернативы, я иногда использую также BigDecimal для точных вычислений.

0 голосов
/ 24 февраля 2020

Выражение левого операнда (floor(value * 100.) % 5) оценивается как double, поэтому в принципе вы проверяете равенство чисел с плавающей запятой, что технически открывает дверь для плавающего точечные ошибки. Однако, согласно документации floor () :

Возвращает наибольшее (ближайшее к положительной бесконечности) двойное значение, которое меньше или равно аргументу и равно равно математическому целому числу .

Таким образом, с учетом этого риск ошибки с плавающей точкой должен быть равен нулю. Тем не менее, double допускает особые случаи (Double.NaN, et c.), С которыми вам, возможно, придется работать, в зависимости от того, что может быть присвоено value.

...