TL; DR
- Используйте следующую функцию вместо принятого в настоящее время решения, чтобы избежать некоторых нежелательных результатов в определенных предельных случаях, будучи потенциально более эффективным.
- Знайте ожидаемую неточность ваших чисел и введите их соответствующим образом в функции сравнения.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
// those defaults are arbitrary and could be removed
{
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
. (Я взял epsilon
0,5 для иллюстрации).
![enter image description here](https://i.stack.imgur.com/i69HS.png)
Относительный режим - это то, что используется для «нормальных» или «достаточно больших» значений с плавающей запятой. (Подробнее об этом позже).
Второй режим - абсолютный , когда мы просто сравниваем их разницу с фиксированным числом. Это дает следующий профиль (снова с epsilon
0,5 и relth
1 для иллюстрации).
![enter image description here](https://i.stack.imgur.com/v8j4I.png)
Этот абсолютный режим сравнения используется для «крошечных» значений с плавающей запятой.
Теперь вопрос в том, как мы можем соединить эти два шаблона ответа.
В ответе Майкла Боргвардта, переключатель основан на значении diff
, которое должно быть ниже relth
(Float.MIN_NORMAL
в его ответе). Эта зона переключения показана заштрихованной на графике ниже.
![enter image description here](https://i.stack.imgur.com/elpMZ.png)
Поскольку relth * epsilon
меньше, чем relth
, зеленые пятна не слипаются, что, в свою очередь, придает решению плохое свойство: мы можем найти тройки чисел, такие что x < y_1 < y_2
и все же x == y2
, но x != y1
.
![enter image description here](https://i.stack.imgur.com/mSWa0.png)
Возьмите этот яркий пример:
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|
, которое представлено заштрихованным квадратом ниже. Это гарантирует, что обе зоны соединяются изящно.
![enter image description here](https://i.stack.imgur.com/W7Aan.png)
Кроме того, приведенный выше код не имеет ветвления, что может быть более эффективным. Учтите, что такие операции, как max
и abs
, которые a priori требуют ветвления, часто имеют специальные инструкции по сборке. По этой причине, я думаю, что этот подход превосходит другое решение, которое будет заключаться в исправлении nearlyEqual
Майкла путем изменения переключателя с diff < relth
на diff < eps * relth
, который затем будет производить по существу тот же шаблон ответа.
Где переключаться между относительным и абсолютным сравнением?
Переключение между этими режимами производится около relth
, что в принятом ответе принимается за FLT_MIN
. Этот выбор означает, что представление float32
- это то, что ограничивает точность наших чисел с плавающей запятой.
Это не всегда имеет смысл. Например, если сравниваемые вами числа являются результатом вычитания, возможно, что-то в диапазоне FLT_EPSILON
имеет больше смысла. Если они представляют собой квадратные корни из вычитаемых чисел, численная неточность может быть даже выше.
Это довольно очевидно, если рассмотреть сравнение с плавающей запятой с 0
. Здесь любое относительное сравнение не удастся, потому что |x - 0| / (|x| + 0) = 1
. Таким образом, для сравнения необходимо переключиться в абсолютный режим, когда x
имеет порядок неточности в ваших вычислениях - и редко бывает настолько низким, как FLT_MIN
.
Это причина для введения параметра relth
выше.
Кроме того, не умножая relth
на epsilon
, интерпретация этого параметра проста и соответствует уровню числовой точности, который мы ожидаем от этих чисел.
Математическое урчание
(хранится здесь в основном для собственного удовольствия)
В более общем смысле я предполагаю, что у хорошо себя работающего оператора сравнения с плавающей запятой =~
должны быть некоторые основные свойства.
Следующее довольно очевидно:
- self-равенства:
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
(равенстводля данной неточности подразумевается равенство при более высокой неточности)
Предлагаемое решение также проверяет их.