Эта тема много раз возникала в StackOverflow, но я считаю, что это новый подход. Да, я прочитал статьи Брюса Доусона и Что каждый компьютерный ученый должен Знайте о арифметике с плавающей точкой и этот хороший ответ.
Как я понимаю, в типичной системе есть четыре основные проблемы при сравнении чисел с плавающей запятой для равенства:
- Расчеты с плавающей точкой не являются точными.
- Независимо от того,
a-b
"маленький" зависит от шкалыa
иb
- Независимо от того,
a-b
"small" зависит от типаa
иb
(например, float, double, long double) - Плавающая точка обычно имеет + -инфекцию, NaN и денормализованные представления, любые из которых могут мешать разработке na & iuml; ve
Этот ответ - ака. "подход Google" - кажется, популярен. Он справляется со всеми сложными делами. И это очень точно масштабирует сравнение, проверяя, находятся ли два значения в пределах фиксированного числа ULPs друг друга. Так, например, очень большое число сравнивает "почти равное" с бесконечностью.
Однако:
- Это очень грязно, на мой взгляд.
- Он не особенно переносим, сильно полагаясь на внутренние представления, используя объединение для чтения бит из поплавка и т.д.
- Он обрабатывает только IEEE 754 с одноточечной и двойной точностью (в частности, не длинный двойной x86)
Мне нужно что-то подобное, но с использованием стандартного С++ и обработки длинных удвоений. Под "стандартным", я имею в виду С++ 03, если это возможно, и С++ 11, если это необходимо.
Вот моя попытка.
#include <cmath>
#include <limits>
#include <algorithm>
namespace {
// Local version of frexp() that handles infinities specially.
template<typename T>
T my_frexp(const T num, int *exp)
{
typedef std::numeric_limits<T> limits;
// Treat +-infinity as +-(2^max_exponent).
if (std::abs(num) > limits::max())
{
*exp = limits::max_exponent + 1;
return std::copysign(0.5, num);
}
else return std::frexp(num, exp);
}
}
template<typename T>
bool almostEqual(const T a, const T b, const unsigned ulps=4)
{
// Handle NaN.
if (std::isnan(a) || std::isnan(b))
return false;
typedef std::numeric_limits<T> limits;
// Handle very small and exactly equal values.
if (std::abs(a-b) <= ulps * limits::denorm_min())
return true;
// frexp() does the wrong thing for zero. But if we get this far
// and either number is zero, then the other is too big, so just
// handle that now.
if (a == 0 || b == 0)
return false;
// Break the numbers into significand and exponent, sorting them by
// exponent.
int min_exp, max_exp;
T min_frac = my_frexp(a, &min_exp);
T max_frac = my_frexp(b, &max_exp);
if (min_exp > max_exp)
{
std::swap(min_frac, max_frac);
std::swap(min_exp, max_exp);
}
// Convert the smaller to the scale of the larger by adjusting its
// significand.
const T scaled_min_frac = std::ldexp(min_frac, min_exp-max_exp);
// Since the significands are now in the same scale, and the larger
// is in the range [0.5, 1), 1 ulp is just epsilon/2.
return std::abs(max_frac-scaled_min_frac) <= ulps * limits::epsilon() / 2;
}
Я утверждаю, что этот код (а) обрабатывает все соответствующие случаи, (б) выполняет то же самое, что и реализация Google для одно- и двухточечной обработки IEEE-754, и (в) является совершенно стандартным С++.
Одна или несколько из этих претензий почти наверняка ошибочны. Я соглашусь на любой ответ, который демонстрирует такое, желательно с исправлением. Хороший ответ должен включать один или несколько из:
- Конкретные входы, отличающиеся более чем на
ulps
Единицы в последнем месте, но для которых эта функция возвращает true (чем больше разница, тем лучше) - Конкретные входы, отличающиеся на
ulps
Единицы в последнем месте, но для которых эта функция возвращает false (чем меньше разница, тем лучше) - Любые случаи, когда я пропустил
- Любой способ, которым этот код опирается на поведение undefined или ломается в зависимости от поведения, определенного реализацией. (Если возможно, процитируйте соответствующую спецификацию.)
- Исправления для любых проблем, которые вы идентифицируете.
- Любой способ упростить код, не нарушая его.
Я намерен разместить нетривиальную награду по этому вопросу.