Какой самый эффективный способ сравнения с плавающей запятой и двойного сравнения? - PullRequest
477 голосов
/ 20 августа 2008

Какой самый эффективный способ сравнить два double или два float значения?

Просто делать это не правильно:

bool CompareDoubles1 (double A, double B)
{
   return A == B;
}

Но что-то вроде:

bool CompareDoubles2 (double A, double B) 
{
   diff = A - B;
   return (diff < EPSILON) && (-diff < EPSILON);
}

Кажется, переработка отходов.

Кто-нибудь знает более умный компаратор поплавка?

Ответы [ 27 ]

417 голосов
/ 17 сентября 2008

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

Я провел много времени, отслеживая ошибки в системе, которая предполагала a==b, если |a-b|<epsilon. Основными проблемами были:

  1. Неявная презумпция в алгоритме, что если a==b и b==c, то a==c.

  2. Использование одного и того же эпсилона для линий, измеренных в дюймах, и линий, измеренных в милях (.001 дюйма). Это a==b, но 1000a!=1000b. (Вот почему почтиEqual2sComplement запрашивает epsilon или max ULPS).

  3. Использование одного и того же эпсилона как для косинуса углов, так и для длины линий!

  4. Использование такой функции сравнения для сортировки элементов в коллекции. (В этом случае использование встроенного оператора C ++ == для двойных значений дает правильные результаты.)

Как я уже сказал: все зависит от контекста и ожидаемого размера a и b.

Кстати, std::numeric_limits<double>::epsilon() - это «эпсилон машины». Это разница между 1.0 и следующим значением, представленным двойным. Я предполагаю, что это можно использовать в функции сравнения, но только если ожидаемые значения меньше 1. (Это в ответ на ответ @ cdv ...)

Кроме того, если у вас есть int арифметика в doubles (здесь мы используем double для хранения значений int в некоторых случаях), ваша арифметика будет правильной. Например, 4.0 / 2.0 будет таким же, как 1.0 + 1.0. Это до тех пор, пока вы не делаете вещи, которые приводят к дробным частям (4.0 / 3.0) или не выходят за пределы размера int.

171 голосов
/ 20 августа 2008

Сравнение со значением эпсилона - это то, что делает большинство людей (даже в программировании игр).

Вы должны немного изменить свою реализацию:

bool AreSame(double a, double b)
{
    return fabs(a - b) < EPSILON;
}

Редактировать: Кристер добавил стопку отличной информации по этой теме в недавнем сообщении в блоге . Наслаждайтесь.

111 голосов
/ 06 августа 2010

Я обнаружил, что Google C ++ Testing Framework содержит хорошую кросс-платформенную реализацию ПочтиEqual2sComplement на основе шаблонов, которая работает как с двойными, так и с плавающими числами. Учитывая, что он выпущен под лицензией BSD, использование его в вашем собственном коде не должно быть проблемой, если вы сохраняете лицензию. Я извлек следующий код из http://code.google.com/p/googletest/source/browse/trunk/include/gtest/internal/gtest-internal.h https://github.com/google/googletest/blob/master/googletest/include/gtest/internal/gtest-internal.h и добавил лицензию сверху.

Обязательно #define GTEST_OS_WINDOWS определите какое-либо значение (или измените код, в котором он используется, на то, что соответствует вашей кодовой базе - в конце концов, это BSD-лицензия).

Пример использования:

double left  = // something
double right = // something
const FloatingPoint<double> lhs(left), rhs(right);

if (lhs.AlmostEquals(rhs)) {
  //they're equal!
}

Вот код:

// Copyright 2005, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Authors: wan@google.com (Zhanyong Wan), eefacm@gmail.com (Sean Mcafee)
//
// The Google C++ Testing Framework (Google Test)


// This template class serves as a compile-time function from size to
// type.  It maps a size in bytes to a primitive type with that
// size. e.g.
//
//   TypeWithSize<4>::UInt
//
// is typedef-ed to be unsigned int (unsigned integer made up of 4
// bytes).
//
// Such functionality should belong to STL, but I cannot find it
// there.
//
// Google Test uses this class in the implementation of floating-point
// comparison.
//
// For now it only handles UInt (unsigned int) as that's all Google Test
// needs.  Other types can be easily added in the future if need
// arises.
template <size_t size>
class TypeWithSize {
 public:
  // This prevents the user from using TypeWithSize<N> with incorrect
  // values of N.
  typedef void UInt;
};

// The specialization for size 4.
template <>
class TypeWithSize<4> {
 public:
  // unsigned int has size 4 in both gcc and MSVC.
  //
  // As base/basictypes.h doesn't compile on Windows, we cannot use
  // uint32, uint64, and etc here.
  typedef int Int;
  typedef unsigned int UInt;
};

// The specialization for size 8.
template <>
class TypeWithSize<8> {
 public:
#if GTEST_OS_WINDOWS
  typedef __int64 Int;
  typedef unsigned __int64 UInt;
#else
  typedef long long Int;  // NOLINT
  typedef unsigned long long UInt;  // NOLINT
#endif  // GTEST_OS_WINDOWS
};


// This template class represents an IEEE floating-point number
// (either single-precision or double-precision, depending on the
// template parameters).
//
// The purpose of this class is to do more sophisticated number
// comparison.  (Due to round-off error, etc, it's very unlikely that
// two floating-points will be equal exactly.  Hence a naive
// comparison by the == operation often doesn't work.)
//
// Format of IEEE floating-point:
//
//   The most-significant bit being the leftmost, an IEEE
//   floating-point looks like
//
//     sign_bit exponent_bits fraction_bits
//
//   Here, sign_bit is a single bit that designates the sign of the
//   number.
//
//   For float, there are 8 exponent bits and 23 fraction bits.
//
//   For double, there are 11 exponent bits and 52 fraction bits.
//
//   More details can be found at
//   http://en.wikipedia.org/wiki/IEEE_floating-point_standard.
//
// Template parameter:
//
//   RawType: the raw floating-point type (either float or double)
template <typename RawType>
class FloatingPoint {
 public:
  // Defines the unsigned integer type that has the same size as the
  // floating point number.
  typedef typename TypeWithSize<sizeof(RawType)>::UInt Bits;

  // Constants.

  // # of bits in a number.
  static const size_t kBitCount = 8*sizeof(RawType);

  // # of fraction bits in a number.
  static const size_t kFractionBitCount =
    std::numeric_limits<RawType>::digits - 1;

  // # of exponent bits in a number.
  static const size_t kExponentBitCount = kBitCount - 1 - kFractionBitCount;

  // The mask for the sign bit.
  static const Bits kSignBitMask = static_cast<Bits>(1) << (kBitCount - 1);

  // The mask for the fraction bits.
  static const Bits kFractionBitMask =
    ~static_cast<Bits>(0) >> (kExponentBitCount + 1);

  // The mask for the exponent bits.
  static const Bits kExponentBitMask = ~(kSignBitMask | kFractionBitMask);

  // How many ULP's (Units in the Last Place) we want to tolerate when
  // comparing two numbers.  The larger the value, the more error we
  // allow.  A 0 value means that two numbers must be exactly the same
  // to be considered equal.
  //
  // The maximum error of a single floating-point operation is 0.5
  // units in the last place.  On Intel CPU's, all floating-point
  // calculations are done with 80-bit precision, while double has 64
  // bits.  Therefore, 4 should be enough for ordinary use.
  //
  // See the following article for more details on ULP:
  // http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm.
  static const size_t kMaxUlps = 4;

  // Constructs a FloatingPoint from a raw floating-point number.
  //
  // On an Intel CPU, passing a non-normalized NAN (Not a Number)
  // around may change its bits, although the new value is guaranteed
  // to be also a NAN.  Therefore, don't expect this constructor to
  // preserve the bits in x when x is a NAN.
  explicit FloatingPoint(const RawType& x) { u_.value_ = x; }

  // Static methods

  // Reinterprets a bit pattern as a floating-point number.
  //
  // This function is needed to test the AlmostEquals() method.
  static RawType ReinterpretBits(const Bits bits) {
    FloatingPoint fp(0);
    fp.u_.bits_ = bits;
    return fp.u_.value_;
  }

  // Returns the floating-point number that represent positive infinity.
  static RawType Infinity() {
    return ReinterpretBits(kExponentBitMask);
  }

  // Non-static methods

  // Returns the bits that represents this number.
  const Bits &bits() const { return u_.bits_; }

  // Returns the exponent bits of this number.
  Bits exponent_bits() const { return kExponentBitMask & u_.bits_; }

  // Returns the fraction bits of this number.
  Bits fraction_bits() const { return kFractionBitMask & u_.bits_; }

  // Returns the sign bit of this number.
  Bits sign_bit() const { return kSignBitMask & u_.bits_; }

  // Returns true iff this is NAN (not a number).
  bool is_nan() const {
    // It's a NAN if the exponent bits are all ones and the fraction
    // bits are not entirely zeros.
    return (exponent_bits() == kExponentBitMask) && (fraction_bits() != 0);
  }

  // Returns true iff this number is at most kMaxUlps ULP's away from
  // rhs.  In particular, this function:
  //
  //   - returns false if either number is (or both are) NAN.
  //   - treats really large numbers as almost equal to infinity.
  //   - thinks +0.0 and -0.0 are 0 DLP's apart.
  bool AlmostEquals(const FloatingPoint& rhs) const {
    // The IEEE standard says that any comparison operation involving
    // a NAN must return false.
    if (is_nan() || rhs.is_nan()) return false;

    return DistanceBetweenSignAndMagnitudeNumbers(u_.bits_, rhs.u_.bits_)
        <= kMaxUlps;
  }

 private:
  // The data type used to store the actual floating-point number.
  union FloatingPointUnion {
    RawType value_;  // The raw floating-point number.
    Bits bits_;      // The bits that represent the number.
  };

  // Converts an integer from the sign-and-magnitude representation to
  // the biased representation.  More precisely, let N be 2 to the
  // power of (kBitCount - 1), an integer x is represented by the
  // unsigned number x + N.
  //
  // For instance,
  //
  //   -N + 1 (the most negative number representable using
  //          sign-and-magnitude) is represented by 1;
  //   0      is represented by N; and
  //   N - 1  (the biggest number representable using
  //          sign-and-magnitude) is represented by 2N - 1.
  //
  // Read http://en.wikipedia.org/wiki/Signed_number_representations
  // for more details on signed number representations.
  static Bits SignAndMagnitudeToBiased(const Bits &sam) {
    if (kSignBitMask & sam) {
      // sam represents a negative number.
      return ~sam + 1;
    } else {
      // sam represents a positive number.
      return kSignBitMask | sam;
    }
  }

  // Given two numbers in the sign-and-magnitude representation,
  // returns the distance between them as an unsigned number.
  static Bits DistanceBetweenSignAndMagnitudeNumbers(const Bits &sam1,
                                                     const Bits &sam2) {
    const Bits biased1 = SignAndMagnitudeToBiased(sam1);
    const Bits biased2 = SignAndMagnitudeToBiased(sam2);
    return (biased1 >= biased2) ? (biased1 - biased2) : (biased2 - biased1);
  }

  FloatingPointUnion u_;
};

РЕДАКТИРОВАТЬ: Этому посту 4 года. Это, вероятно, все еще в силе, и код хорош, но некоторые люди нашли улучшения. Лучше всего получить последнюю версию AlmostEquals прямо из исходного кода Google Test, а не ту, которую я вставил сюда.

92 голосов
/ 31 октября 2008

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

Сравнение чисел с плавающей запятой Брюса Доусона - хорошее место, чтобы начать с сравнения с плавающей запятой.

Следующие определения взяты из Искусство компьютерного программирования от Кнута :

bool approximatelyEqual(float a, float b, float epsilon)
{
    return fabs(a - b) <= ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool essentiallyEqual(float a, float b, float epsilon)
{
    return fabs(a - b) <= ( (fabs(a) > fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool definitelyGreaterThan(float a, float b, float epsilon)
{
    return (a - b) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

bool definitelyLessThan(float a, float b, float epsilon)
{
    return (b - a) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
}

Конечно, выбор эпсилона зависит от контекста и определяет, насколько равными должны быть числа.

Другой метод сравнения чисел с плавающей запятой состоит в том, чтобы посмотреть на ULP (единицы на последнем месте) чисел. Не говоря конкретно о сравнениях, статья То, что должен знать каждый компьютерный специалист о числах с плавающей запятой , является хорошим ресурсом для понимания того, как работает с плавающей запятой и каковы подводные камни, включая то, что такое ULP.

46 голосов
/ 20 августа 2008

Для более глубокого подхода прочитайте Сравнение чисел с плавающей запятой . Вот фрагмент кода по этой ссылке:

// Usable AlmostEqual function    
bool AlmostEqual2sComplement(float A, float B, int maxUlps)    
{    
    // Make sure maxUlps is non-negative and small enough that the    
    // default NAN won't compare as equal to anything.    
    assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024);    
    int aInt = *(int*)&A;    
    // Make aInt lexicographically ordered as a twos-complement int    
    if (aInt < 0)    
        aInt = 0x80000000 - aInt;    
    // Make bInt lexicographically ordered as a twos-complement int    
    int bInt = *(int*)&B;    
    if (bInt < 0)    
        bInt = 0x80000000 - bInt;    
    int intDiff = abs(aInt - bInt);    
    if (intDiff <= maxUlps)    
        return true;    
    return false;    
}
27 голосов
/ 01 сентября 2008

Портативный способ получить эпсилон в C ++ -

#include <limits>
std::numeric_limits<double>::epsilon()

Тогда функция сравнения становится

#include <cmath>
#include <limits>

bool AreSame(double a, double b) {
    return std::fabs(a - b) < std::numeric_limits<double>::epsilon();
}
24 голосов
/ 22 февраля 2013

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

Мы можем найти несколько более практичную статью в Пересмотренные допуски с плавающей точкой и отмечает, что есть абсолютный допуск тест, который сводится к этому в C ++:

bool absoluteToleranceCompare(double x, double y)
{
    return std::fabs(x - y) <= std::numeric_limits<double>::epsilon() ;
}

и относительный допуск тест:

bool relativeToleranceCompare(double x, double y)
{
    double maxXY = std::max( std::fabs(x) , std::fabs(y) ) ;
    return std::fabs(x - y) <= std::numeric_limits<double>::epsilon()*maxXY ;
}

В статье отмечается, что абсолютный тест не проходит, когда x и y большие, и не выполняется в относительном случае, когда они малы. Если предположить, что абсолютная и относительная толерантность одинаковы, комбинированный тест будет выглядеть так:

bool combinedToleranceCompare(double x, double y)
{
    double maxXYOne = std::max( { 1.0, std::fabs(x) , std::fabs(y) } ) ;

    return std::fabs(x - y) <= std::numeric_limits<double>::epsilon()*maxXYOne ;
}
14 голосов
/ 20 августа 2008

Код, который вы написали, содержит ошибки:

return (diff < EPSILON) && (-diff > EPSILON);

Правильный код будет:

return (diff < EPSILON) && (diff > -EPSILON);

(... и да, это другое)

Интересно, не заставят ли вас в какой-то степени потерпеть неудачу в оценках? Я бы сказал, что это зависит от компилятора. Вы можете попробовать оба. Если они в среднем эквивалентны, возьмите реализацию с fabs.

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

Наконец, вы можете получить лучший результат, вставив эту функцию. Хотя вряд ли что-то улучшится ...

Редактировать: OJ, спасибо за исправление вашего кода. Я стер свой комментарий соответственно

14 голосов
/ 01 сентября 2008

`возврат потрясающих (a - b)

Это хорошо, если:

  • порядок величины ваших входов не сильно меняется
  • очень малое количество противоположных знаков можно рассматривать как равные

Но в противном случае это приведет к неприятностям. Числа двойной точности имеют разрешение около 16 десятичных знаков. Если сравниваемые два числа больше по величине, чем EPSILON * 1.0E16, то вы можете также сказать:

return a==b;

Я рассмотрю другой подход, который предполагает, что вам нужно беспокоиться о первой проблеме, и предположим, что вторая подходит для вашего приложения. Решение будет что-то вроде:

#define VERYSMALL  (1.0E-150)
#define EPSILON    (1.0E-8)
bool AreSame(double a, double b)
{
    double absDiff = fabs(a - b);
    if (absDiff < VERYSMALL)
    {
        return true;
    }

    double maxAbs  = max(fabs(a) - fabs(b));
    return (absDiff/maxAbs) < EPSILON;
}

Это дорого в вычислительном отношении, но иногда это то, что требуется. Это то, что мы должны делать в моей компании, потому что мы имеем дело с инженерной библиотекой, и входные данные могут варьироваться на несколько десятков порядков.

В любом случае, суть заключается в следующем (и относится практически к любой проблеме программирования): оцените свои потребности, а затем найдите решение, отвечающее вашим потребностям - не думайте, что простой ответ удовлетворит ваши потребности. Если после вашей оценки вы обнаружите, что fabs(a-b) < EPSILON будет достаточно, идеально - используйте его! Но помните о его недостатках и других возможных решениях.

8 голосов
/ 31 декабря 2016

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

Краткое резюме

  1. При сравнении с плавающей запятой есть две проблемы: у вас ограниченная точность, а значение «приблизительно ноль» зависит от контекста (см. Следующий пункт).
  2. 1E-8 приблизительно такой же, как 1E-16? Если вы смотрите на данные датчика с шумом, то, вероятно, да, но если вы делаете молекулярное моделирование, то, возможно, нет! Итог: вам всегда нужно думать о значении толерантности в контексте вызова конкретной функции, а не просто делать его общей жестко закодированной константой приложения.
  3. Для общих библиотечных функций по-прежнему хорошо иметь параметр с допуск по умолчанию . Типичный выбор - numeric_limits::epsilon(), который совпадает с FLT_EPSILON в float.h. Это, однако, проблематично, потому что epsilon для сравнения значений, таких как 1.0, если не совпадает с epsilon для значений, таких как 1E9. FLT_EPSILON определен для 1.0.
  4. Очевидная реализация для проверки, находится ли число в пределах допуска, - fabs(a-b) <= epsilon, однако это не работает, потому что epsilon по умолчанию определен для 1.0. Нам нужно масштабировать эпсилон вверх или вниз с точки зрения a и b.
  5. Есть два решения этой проблемы: либо вы устанавливаете epsilon пропорционально max(a,b), либо вы можете получить следующие представимые числа вокруг a, а затем посмотреть, попадает ли b в этот диапазон. Первый называется «относительным» методом, а позже - методом ULP.
  6. Оба метода на самом деле дают сбой при сравнении с 0. В этом случае приложение должно предоставить правильный допуск.

Реализация служебных функций (C ++ 11)

//implements relative method - do not use for comparing with zero
//use this most of the time, tolerance needs to be meaningful in your context
template<typename TReal>
static bool isApproximatelyEqual(TReal a, TReal b, TReal tolerance = std::numeric_limits<TReal>::epsilon())
{
    TReal diff = std::fabs(a - b);
    if (diff <= tolerance)
        return true;

    if (diff < std::fmax(std::fabs(a), std::fabs(b)) * tolerance)
        return true;

    return false;
}

//supply tolerance that is meaningful in your context
//for example, default tolerance may not work if you are comparing double with float
template<typename TReal>
static bool isApproximatelyZero(TReal a, TReal tolerance = std::numeric_limits<TReal>::epsilon())
{
    if (std::fabs(a) <= tolerance)
        return true;
    return false;
}


//use this when you want to be on safe side
//for example, don't start rover unless signal is above 1
template<typename TReal>
static bool isDefinitelyLessThan(TReal a, TReal b, TReal tolerance = std::numeric_limits<TReal>::epsilon())
{
    TReal diff = a - b;
    if (diff < tolerance)
        return true;

    if (diff < std::fmax(std::fabs(a), std::fabs(b)) * tolerance)
        return true;

    return false;
}
template<typename TReal>
static bool isDefinitelyGreaterThan(TReal a, TReal b, TReal tolerance = std::numeric_limits<TReal>::epsilon())
{
    TReal diff = a - b;
    if (diff > tolerance)
        return true;

    if (diff > std::fmax(std::fabs(a), std::fabs(b)) * tolerance)
        return true;

    return false;
}

//implements ULP method
//use this when you are only concerned about floating point precision issue
//for example, if you want to see if a is 1.0 by checking if its within
//10 closest representable floating point numbers around 1.0.
template<typename TReal>
static bool isWithinPrecisionInterval(TReal a, TReal b, unsigned int interval_size = 1)
{
    TReal min_a = a - (a - std::nextafter(a, std::numeric_limits<TReal>::lowest())) * interval_size;
    TReal max_a = a + (std::nextafter(a, std::numeric_limits<TReal>::max()) - a) * interval_size;

    return min_a <= b && max_a >= b;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...