В настоящее время я пишу код, в котором есть что-то вроде:
double a = SomeCalculation1();
double b = SomeCalculation2();
if (a < b)
DoSomething2();
else if (a > b)
DoSomething3();
А потом в других местах мне может понадобиться выполнить равенство:
double a = SomeCalculation3();
double b = SomeCalculation4();
if (a == 0.0)
DoSomethingUseful(1 / a);
if (b == 0.0)
return 0; // or something else here
Короче говоря, у меня много вычислений с плавающей запятой, и мне нужно проводить различные сравнения условий. Я не могу преобразовать его в целочисленную математику, потому что в данном контексте это бессмысленно.
Я читал раньше, что сравнения с плавающей запятой могут быть ненадежными, так как могут происходить такие вещи, как это:
double a = 1.0 / 3.0;
double b = a + a + a;
if ((3 * a) != b)
Console.WriteLine("Oh no!");
Короче говоря, я хотел бы знать: как я могу надежно сравнивать числа с плавающей запятой (меньше, больше, равенство)?
Диапазон чисел, который я использую, составляет примерно от 10E-14 до 10E6, поэтому мне нужно работать как с маленькими, так и с большими числами.
Я назвал это языковым агностиком, потому что мне интересно, как я могу это сделать, независимо от того, какой язык я использую.
источник
Ответы:
Сравнение на большее / меньшее значение на самом деле не проблема, если вы не работаете прямо на границе ограничения с плавающей / двойной точностью.
Для сравнения «нечеткое равенство» это (Java-код, который должен быть легко адаптируемым) - это то, что я придумал для The Floating-Point Guide после большой работы и с учетом множества критических замечаний:
public static boolean nearlyEqual(float a, float b, float epsilon) { final float absA = Math.abs(a); final float absB = Math.abs(b); final float diff = Math.abs(a - b); if (a == b) { // shortcut, handles infinities return true; } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) { // a or b is zero or both are extremely close to it // relative error is less meaningful here return diff < (epsilon * Float.MIN_NORMAL); } else { // use relative error return diff / (absA + absB) < epsilon; } }
Поставляется с набором тестов. Вы должны немедленно отклонить любое решение, которое не работает, потому что оно практически гарантированно не сработает в некоторых крайних случаях, таких как одно значение 0, два очень маленьких значения, противоположных нулю, или бесконечности.
Альтернативой (см. Ссылку выше для получения дополнительной информации) является преобразование битовых шаблонов чисел с плавающей запятой в целые числа и принятие всего в пределах фиксированного целочисленного расстояния.
В любом случае, вероятно, не существует решения, идеально подходящего для всех приложений. В идеале вы должны разработать / адаптировать свой собственный набор тестов, охватывающий ваши фактические варианты использования.
источник
else if (a * b == 0)
, но тогда ваш комментарий в той же строке естьa or b or both are zero
. Но разве это не две разные вещи? Например, еслиa == 1e-162
иb == 2e-162
тогда условиеa * b == 0
будет истинным.abs(a-b)<eps
ответы здесь. Два вопроса: (1) Не лучше ли заменить все<
s на<=
s, что позволит проводить сравнения «ноль-eps», эквивалентные точным сравнениям? (2) Не лучше ли использоватьdiff < epsilon * (absA + absB);
вместоdiff / (absA + absB) < epsilon;
(последняя строка) -?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()); // or even faster: std::min(std::abs(a + b), std::numeric_limits<float>::max()); // keeping this commented out until I update figures below return diff < std::max(relth, epsilon * norm); }
Графика, пожалуйста?
При сравнении чисел с плавающей запятой есть два «режима».
Первый - относительный режим, в котором разница между
x
иy
считается относительно их амплитуды|x| + |y|
. При построении 2D-графика получается следующий профиль, где зеленый цвет означает равенствоx
иy
. (Я взялepsilon
0,5 для иллюстрации).Относительный режим - это то, что используется для "нормальных" или "достаточно больших" значений с плавающей запятой. (Подробнее об этом позже).
Второй - абсолютный режим, когда мы просто сравниваем их разницу с фиксированным числом. Это дает следующий профиль (снова с
epsilon
0,5 иrelth
1 для иллюстрации).Этот абсолютный режим сравнения используется для "крошечных" значений с плавающей запятой.
Теперь вопрос в том, как нам соединить эти два образца ответа.
В ответе Майкла Боргвардта переключатель основан на значении
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
(равенство для данной неточности означает равенство с большей погрешностью)Предлагаемое решение также подтверждает это.
источник
(std::abs(a) + std::abs(b))
быть больше чемstd::numeric_limits<float>::max()
?У меня возникла проблема сравнения чисел с плавающей запятой,
A < B
иA > B
вот что, похоже, работает:if(A - B < Epsilon) && (fabs(A-B) > Epsilon) { printf("A is less than B"); } if (A - B > Epsilon) && (fabs(A-B) > Epsilon) { printf("A is greater than B"); }
Fabs - абсолютная ценность - заботится о том, равны ли они по существу.
источник
fabs
if (A - B < -Epsilon)
Мы должны выбрать уровень допуска для сравнения чисел с плавающей запятой. Например,
final float TOLERANCE = 0.00001; if (Math.abs(f1 - f2) < TOLERANCE) Console.WriteLine("Oh yes!");
Одна запись. Ваш пример довольно забавен.
double a = 1.0 / 3.0; double b = a + a + a; if (a != b) Console.WriteLine("Oh no!");
Немного математики здесь
a = 1/3 b = 1/3 + 1/3 + 1/3 = 1. 1/3 != 1
О да..
Ты имеешь ввиду
if (b != 1) Console.WriteLine("Oh no!")
источник
Идея, которая у меня была для сравнения с плавающей запятой в быстром
infix operator ~= {} func ~= (a: Float, b: Float) -> Bool { return fabsf(a - b) < Float(FLT_EPSILON) } func ~= (a: CGFloat, b: CGFloat) -> Bool { return fabs(a - b) < CGFloat(FLT_EPSILON) } func ~= (a: Double, b: Double) -> Bool { return fabs(a - b) < Double(FLT_EPSILON) }
источник
Адаптация к PHP от Майкла Боргвардта и ответ bosonix:
class Comparison { const MIN_NORMAL = 1.17549435E-38; //from Java Specs // from http://floating-point-gui.de/errors/comparison/ public function nearlyEqual($a, $b, $epsilon = 0.000001) { $absA = abs($a); $absB = abs($b); $diff = abs($a - $b); if ($a == $b) { return true; } else { if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) { return $diff < ($epsilon * self::MIN_NORMAL); } else { return $diff / ($absA + $absB) < $epsilon; } } } }
источник
Вы должны спросить себя, почему вы сравниваете числа. Если вы знаете цель сравнения, вам также следует знать требуемую точность ваших чисел. Это отличается в каждой ситуации и в каждом контексте приложения. Но практически во всех практических случаях требуется абсолютная точность. Относительная точность применима очень редко.
Приведу пример: если ваша цель состоит в том, чтобы нарисовать график на экране, то вы, вероятно, захотите, чтобы значения с плавающей запятой сравнивались одинаково, если они отображаются в один и тот же пиксель на экране. Если размер вашего экрана составляет 1000 пикселей, а ваши числа находятся в диапазоне 1e6, то вы, вероятно, захотите, чтобы 100 для сравнения было равно 200.
При требуемой абсолютной точности алгоритм становится:
источник
Стандартный совет - использовать небольшое значение "эпсилон" (выбираемое, вероятно, в зависимости от вашего приложения) и рассматривать числа с плавающей запятой, которые находятся в пределах эпсилон друг от друга, как равные. например, что-то вроде
#define EPSILON 0.00000001 if ((a - b) < EPSILON && (b - a) < EPSILON) { printf("a and b are about equal\n"); }
Более полный ответ затруднен, потому что ошибка с плавающей запятой очень тонкая и запутанная для рассуждений. Если вы действительно заботитесь о равенстве в каком-либо точном смысле, вы, вероятно, ищете решение, не использующее плавающую точку.
источник
if ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
c
, потому что, как только ваше число станет достаточно большим, EPSILON будет меньше, чем машинная точностьc
. Например, предположимc = 1E+22; d=c/3; e=d+d+d;
. Тогдаe-c
вполне может быть значительно больше 1.double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout<<std::scientific<<a-c;
(a и c не равны согласно pnt и nelhage) илиdouble a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;
(a и b равны согласно pnt и nelhage)Я попытался написать функцию равенства с учетом приведенных выше комментариев. Вот что я придумал:
Изменить: перейти с Math.Max (a, b) на Math.Max (Math.Abs (a), Math.Abs (b))
static bool fpEqual(double a, double b) { double diff = Math.Abs(a - b); double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon; return (diff < epsilon); }
Мысли? Мне все еще нужно проработать больше и меньше.
источник
epsilon
должно бытьMath.abs(Math.Max(a, b)) * Double.Epsilon;
, или всегда будет меньше, чемdiff
для отрицательногоa
иb
. И я думаю, что вашepsilon
слишком мал, функция может не возвращать ничего, отличного от==
оператора. Больше, чем естьa < b && !fpEqual(a,b)
.При этом нужно учитывать, что ошибка усечения относительная. Два числа примерно равны, если их разница примерно равна их ulp (Unit на последнем месте).
Однако, если вы выполняете вычисления с плавающей запятой, ваш потенциал ошибки возрастает с каждой операцией (особенно осторожно с вычитаниями!), Поэтому ваша устойчивость к ошибкам должна соответственно увеличиваться.
источник
Лучший способ сравнить двойники на предмет равенства / неравенства - это взять абсолютное значение их разности и сравнить его с достаточно небольшим (в зависимости от вашего контекста) значением.
double eps = 0.000000001; //for instance double a = someCalc1(); double b = someCalc2(); double diff = Math.abs(a - b); if (diff < eps) { //equal }
источник