Эта тема много раз поднималась в StackOverflow, но я считаю, что это новый подход. Да я прочитал Статьи Брюса Доусона а также Что каждый компьютерщик должен знать об арифметике с плавающей точкой а также этот хороший ответ.
Насколько я понимаю, в типичной системе при сравнении чисел с плавающей точкой на равенство существуют четыре основные проблемы:
a-b
«маленький» зависит от масштаба a
а также b
a-b
«маленький» зависит от типа a
а также b
(например, float, double, long double)Этот ответ — ака. «Подход Google» — похоже, популярен. Он обрабатывает все сложные случаи. И это очень точно масштабирует сравнение, проверяя, находятся ли два значения в пределах фиксированного числа ULPs друг друга. Так, например, очень большое число сравнивается «почти равным» бесконечности.
Тем не мение:
Я хочу что-то подобное, но с использованием стандартного C ++ и обработки длинных пар. Под «стандартом» я подразумеваю C ++ 03, если это возможно, и C ++ 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;
}
Я утверждаю, что этот код (a) обрабатывает все соответствующие случаи, (b) делает то же самое, что и реализация Google для IEEE-754 одинарной и двойной точности, и (c) является совершенно стандартным C ++.
Одно или несколько из этих утверждений почти наверняка неверны. Я приму любой ответ, который демонстрирует такой, желательно с исправлением. Хороший ответ должен включать один или несколько из:
ulps
Единицы в последнем месте, но для которых эта функция возвращает true (чем больше разница, тем лучше)ulps
Единицы в последнем месте, но для которых эта функция возвращает false (чем меньше разница, тем лучше)Я намерен назначить нетривиальную награду за этот вопрос.
4 не является подходящим значением: Ответ, на который вы указываете: «Следовательно, 4 должно быть достаточно для обычного использования», но не содержит никаких оснований для этого утверждения. На самом деле, существуют обычные ситуации, в которых числа, рассчитанные в плавающей точке различными способами, могут различаться во многих ULP, даже если они будут равны, если вычислять с помощью точной математики. Поэтому не должно быть значения по умолчанию для допуска; каждый пользователь должен предоставить свой, надеюсь, на основе тщательного анализа своего кода.
В качестве примера того, почему по умолчанию 4 ULP плохо, рассмотрим 1./49*49-1
, Математически точный результат равен 0, но вычисленный результат (64-разрядный двоичный код IEEE 754) равен -0x1p-53, ошибка превышает 1e307 ULP точного результата и почти 1e16 ULP вычисленного результата.
Иногда значение не подходит: В некоторых случаях допуск не может быть относительным к сравниваемым значениям, ни к математически точному относительному допуску, ни к квантованному допуску ULP. Например, почти каждое выходное значение в БПФ зависит почти от каждого входного значения, а ошибка в любом одном элементе связана с величиной других элементов. Процедура «почти равно» должна содержать дополнительный контекст с информацией о потенциальной ошибке.
«Почти равно» имеет плохие математические свойства: Это показывает один из недостатков «почти равных»: масштабирование изменяет результаты. Код ниже печатает 1 и 0.
double x0 = 1.1;
double x1 = 1.1 + 3*0x1p-52;
std::cout << almostEqual(x0, x1) << "\n";
x0 *= .8;
x1 *= .8;
std::cout << almostEqual(x0, x1) << "\n";
Другой недостаток в том, что он не является переходным; almostEqual(a, b)
а также almostEqual(b, c)
не подразумевает almostEqual(a, c)
,
almostEqual(1.f, 1.f/11, 0x745d17)
неправильно возвращает 1.
1.f / 11 составляет 0x1.745d18p-4. Вычитая это из 1 (что составляет 0x10p-4), получаем 0xe.8ba2e8p-4. Поскольку значение ULP, равное 1, равно 0x1p-23, то есть Ux, равное 0xe.8ba2e8p19 = ULP, равное 0xe8ba2e.8 / 2 (сдвинутое на 20 битов и разделенное на 2, чистое 19 бит) = 0x745d17,4 ULP. Это превышает указанный допуск 0x745d17, поэтому правильный ответ будет 0.
Эта ошибка вызвана округлением max_frac-scaled_min_frac
,
Легкий выход из этой проблемы — указать, что ulps
должно быть меньше чем .5/limits::epsilon
, Затем происходит округление в max_frac-scaled_min_frac
только если разница (даже если округлена) превышает ulps
; если разница меньше этого, вычитание является точным, согласно лемме Стербенса.
Было предложение об использовании long double
чтобы исправить это. Тем не мение, long double
не исправит это. Попробуйте сравнить 1 и -0x1p-149f с ulps, установленным в 1 / limit :: epsilon. Если ваше значение и не имеет 149 бит, результат вычитания округляется до 1, что меньше или равно 1 / limit :: epsilon ULP. И все же математическая разница явно превышает 1.
Выражение factor * limits::epsilon / 2
преобразует коэффициент в тип с плавающей точкой, что приводит к ошибкам округления для больших значений фактора, которые не могут быть точно представлены. Скорее всего, подпрограмма не предназначена для использования с такими большими значениями (миллионы ULP в float), поэтому ее следует указывать как ограничение подпрограммы, а не как ошибку.
Упрощение: Вы могли бы избежать my_frexp, отбросив сначала все неконечные случаи:
if( ! std::isfinite(a) || ! std::isfinite(b) )
return a == b;
Кажется, что isfinite есть в C ++ 11 как минимум
РЕДАКТИРОВАТЬ Однако, если намерение состоит в том, чтобы limits::infinity()
в течение 1 часа limits::max()
тогда приведенное выше упрощение не выполняется, но не должно возвращать my_frexp () limits::max_exponent+1
в *exp
вместо max_exponent + 2?
БУДУЩАЯ ЗАЩИТА: Если вы когда-нибудь захотите расширить такое сравнение до десятичного числа с плавающей точкой http://en.wikipedia.org/wiki/Decimal64_floating-point_format в будущем, и при условии, что ldexp () и frexp () будут обрабатывать такой тип с правильным основанием, то, строго говоря, 0,5 в return std::copysign(0.5, num);
должен быть заменен T(1)/limits::radix()
— или же std::ldexp(T(1),-1)
или что-то … (я не мог найти удобную константу в std :: numeric_limits)
РЕДАКТИРОВАТЬ Как прокомментировал Немо, предположения, что ldexp и frexp будут использовать правильный FLOAT_RADIX, ложны, они придерживаются 2 …
Таким образом, портативная версия Future Proof должна также использовать:
std::scalbn(x,n)
вместо std::ldexp(x,n)
exp=std::ilogb(std::abs(x)),y=std::scalbn(x,-exp)
вместо y=frexp(x,&exp)
теперь, когда выше y in равно [1, FLOAT_RADIX) вместо [T (1) / Float_Radix, 1), возвращаем copysign(T(1),num)
вместо 0,5 для бесконечного случая my_frexp, и тест для ulps*limits::epsilon()
вместо ulps * epsilon () / 2
Это также требует стандарта> = C ++ 11