TL; DR
- Используйте следующую функцию вместо принятого в настоящее время решения, чтобы избежать некоторых нежелательных результатов в определенных предельных случаях, но при этом потенциально более эффективно.
- Знайте ожидаемую неточность ваших чисел и соответствующим образом скормите их в функции сравнения.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
Графика, пожалуйста?
При сравнении чисел с плавающей запятой есть два «режима».
Первый - относительный режим, в котором разница между xи yсчитается относительно их амплитуды |x| + |y|. При построении 2D-графика получается следующий профиль, где зеленый цвет означает равенство xи y. (Я взял epsilon0,5 для иллюстрации).

Относительный режим - это то, что используется для "нормальных" или "достаточно больших" значений с плавающей запятой. (Подробнее об этом позже).
Второй - абсолютный режим, когда мы просто сравниваем их разницу с фиксированным числом. Это дает следующий профиль (снова с epsilon0,5 и relth1 для иллюстрации).

Этот абсолютный режим сравнения используется для "крошечных" значений с плавающей запятой.
Теперь вопрос в том, как нам соединить эти два образца ответа.
В ответе Майкла Боргвардта переключатель основан на значении diff, которое должно быть ниже relth( Float.MIN_NORMALв его ответе). Эта зона переключения показана штриховкой на графике ниже.

Поскольку relth * epsilonэто меньше relth, зеленые пятна не слипаются, что, в свою очередь, придает решению плохое свойство: мы можем найти тройки чисел такие, что x < y_1 < y_2и все же x == y2но x != y1.

Вот яркий пример:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
У нас есть x < y1 < y2, а на самом деле y2 - xболее чем в 2000 раз больше y1 - x. И все же с текущим решением,
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
Напротив, в предложенном выше решении зона переключения основана на значении |x| + |y|, которое представлено ниже заштрихованным квадратом. Это гарантирует изящное соединение обеих зон.

Кроме того, в приведенном выше коде нет ветвления, что могло бы быть более эффективным. Учтите, что такие операции, как maxи abs, которые априори требуют ветвления, часто имеют специальные инструкции по сборке. По этой причине я думаю, что этот подход превосходит другое решение, которое заключалось бы в том, чтобы исправить ошибку Майкла, nearlyEqualизменив переключатель с diff < relthна diff < eps * relth, что затем дало бы по существу тот же шаблон ответа.
Где переключаться между относительным и абсолютным сравнением?
Переключение между этими режимами происходит примерно так relth, как FLT_MINв принятом ответе. Этот выбор означает, что именно представление float32ограничивает точность наших чисел с плавающей запятой.
Это не всегда имеет смысл. Например, если сравниваемые числа являются результатами вычитания, возможно, что-то в диапазоне FLT_EPSILONимеет больше смысла. Если они представляют собой квадрат корней из вычтенных чисел, числовая неточность может быть еще выше.
Это довольно очевидно, если учесть сравнение с плавающей запятой 0. Здесь любое относительное сравнение не удастся, потому что |x - 0| / (|x| + 0) = 1. Таким образом, сравнение должно переключаться в абсолютный режим, когда xэто порядка неточности ваших вычислений - и редко бывает так мало, как FLT_MIN.
Это причина введения указанного relthвыше параметра.
Кроме того, без умножения relthна epsilon, интерпретация этого параметра проста и соответствует уровню числовой точности, который мы ожидаем от этих чисел.
Математическое урчание
(хранится здесь в основном для собственного удовольствия)
В более общем плане я предполагаю, что хорошо работающий оператор сравнения с плавающей запятой =~должен обладать некоторыми основными свойствами.
Достаточно очевидны следующие утверждения:
- самодостаточность:
a =~ a
- симметрия:
a =~ bподразумеваетb =~ a
- инвариантность по оппозиции:
a =~ bподразумевает-a =~ -b
(У нас нет a =~ bи b =~ cподразумевается a =~ c, что =~это не отношение эквивалентности).
Я бы добавил следующие свойства, которые более специфичны для сравнений с плавающей запятой
- если
a < b < c, то a =~ cподразумевает a =~ b(более близкие значения также должны быть равны)
- если
a, b, m >= 0тогда a =~ bподразумевает a + m =~ b + m(большие значения с той же разницей также должны быть равны)
- если
0 <= λ < 1тогда a =~ bподразумевает λa =~ λb(возможно, менее очевидный аргумент в пользу).
Эти свойства уже накладывают сильные ограничения на возможные функции, близкие к равенству. Предлагаемая выше функция проверяет их. Возможно, отсутствует одно или несколько очевидных свойств.
Если представить =~себе семью отношений равенства, =~[Ɛ,t]параметризованных с помощью Ɛи relth, можно также добавить
- если
Ɛ1 < Ɛ2то a =~[Ɛ1,t] bподразумевает a =~[Ɛ2,t] b(равенство для данного допуска подразумевает равенство при более высоком допуске)
- если
t1 < t2тогда a =~[Ɛ,t1] bподразумевает a =~[Ɛ,t2] b(равенство для данной неточности означает равенство с большей погрешностью)
Предлагаемое решение также подтверждает это.