Как мне сделать сравнение с плавающей запятой? - PullRequest
72 голосов
/ 06 февраля 2011

Я сейчас пишу некоторый код, где у меня есть что-то вроде:

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, поэтому мне нужно работать как с маленькими, так и с большими числами.

Я отметил это как независимость от языка, потому что мне интересно, как я могу достичьэто неважно на каком языке я использую.

Ответы [ 11 ]

63 голосов
/ 06 февраля 2011

Сравнение большего / меньшего не является проблемой, если вы не работаете прямо на границе предела с плавающей запятой / двойной точности.

Для сравнения «нечетких равных» это (код Java долженбыть легко адаптируемым) это то, что я придумал для Руководство с плавающей запятой после большой работы и с учетом большого количества критики:

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, два очень маленьких значения, противоположных нулю, или бесконечности.

Альтернатива (см. Ссылкувыше для более подробной информации) - преобразовать битовые комбинации чисел с плавающей точкой в ​​целое и принять все в пределах фиксированного целочисленного расстояния.

В любом случае, вероятно, не существует решения, которое идеально подходит для всех приложений.В идеале вы должны разработать / адаптировать свой собственный набор тестов, охватывающий ваши реальные случаи использования.

12 голосов
/ 13 октября 2012

У меня была проблема сравнения чисел с плавающей запятой 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");
}

Фабс - абсолютное значение - заботится, если они по существуравны.

10 голосов
/ 01 сентября 2015

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

Относительный режим - это то, что используется для «нормальных» или «достаточно больших» значений с плавающей запятой. (Подробнее об этом позже).

Второй режим - абсолютный , когда мы просто сравниваем их разницу с фиксированным числом. Это дает следующий профиль (снова с epsilon 0,5 и relth 1 для иллюстрации).

enter image description here

Этот абсолютный режим сравнения используется для «крошечных» значений с плавающей запятой.

Теперь вопрос в том, как мы можем соединить эти два шаблона ответа.

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

enter image description here

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

enter image description here

Возьмите этот яркий пример:

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

Кроме того, приведенный выше код не имеет ветвления, что может быть более эффективным. Учтите, что такие операции, как 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 (равенстводля данной неточности подразумевается равенство при более высокой неточности)

Предлагаемое решение также проверяет их.

10 голосов
/ 27 сентября 2013

Мы должны выбрать уровень допуска для сравнения чисел с плавающей точкой. Например,

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!")
3 голосов
/ 24 июня 2015

Идея, которую я имел для сравнения с плавающей точкой в ​​Swift

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)
}
1 голос
/ 16 января 2015

Адаптация к PHP от ответа Михаила Боргвардта и Бозоникса:

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;
            }
        }
    }
}
0 голосов
/ 20 августа 2018

Вы должны спросить себя, почему вы сравниваете числа.Если вы знаете цель сравнения, то вам также следует знать требуемую точность ваших чисел.Это отличается в каждой ситуации и в каждом контексте приложения.Но почти во всех практических случаях требуется абсолютная точность.Относительная точность применима только очень редко.

В качестве примера: если ваша цель - нарисовать график на экране, то вы, вероятно, хотите, чтобы значения с плавающей запятой сравнивались равными, если они отображаются на один и тот же пиксель на экране.Если размер вашего экрана составляет 1000 пикселей, а ваши числа находятся в диапазоне 1e6, то вы, вероятно, захотите, чтобы сравнение 100 равнялось 200.

Учитывая требуемую абсолютную точность, тогда алгоритм становится следующим:

public static ComparisonResult compare(float a, float b, float accuracy) 
{
    if (isnan(a) || isnan(b))   // if NaN needs to be supported
        return UNORDERED;    
    if (a == b)                 // short-cut and takes care of infinities
        return EQUAL;           
    if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
        return EQUAL;
    if (a < b)                  // larger / smaller
        return SMALLER;
    else
        return LARGER;
}
0 голосов
/ 06 февраля 2011

Я попытался написать функцию равенства с учетом вышеупомянутых комментариев.Вот что я придумал:

Редактировать: изменить с 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);
}

Мысли?Мне все еще нужно работать больше, чем меньше, а также.

0 голосов
/ 06 февраля 2011

Необходимо учитывать, что ошибка усечения является относительной.Два числа примерно равны, если их разность примерно равна их величине ulp (Единица на последнем месте).

Однако, если вы выполняете вычисления с плавающей запятой, ваш потенциал ошибок увеличивается с каждой операцией (особенно осторожнос вычитаниями!), поэтому ваша устойчивость к ошибкам должна соответственно увеличиться.

0 голосов
/ 06 февраля 2011

Лучший способ сравнить двойники на равенство / неравенство - взять абсолютное значение их разности и сравнить его с достаточно малым (в зависимости от вашего контекста) значением.

double eps = 0.000000001; //for instance

double a = someCalc1();
double b = someCalc2();

double diff = Math.abs(a - b);
if (diff < eps) {
    //equal
}
...