Сравнение плавающих точек
Эта тема много раз возникала в 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 или ломается в зависимости от поведения, определенного реализацией. (Если возможно, процитируйте соответствующую спецификацию.)
- Исправления для любых проблем, которые вы идентифицируете.
- Любой способ упростить код, не нарушая его.
Я намерен разместить нетривиальную награду по этому вопросу.
Ответы
Ответ 1
"Почти равно" не является хорошей функцией
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, то есть 0xe.8ba2e8p19 ULP = 0xe8ba2e.8/2 ULP (сдвинутые 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/пределам: epsilon. Если ваше значение не имеет 149 бит, результат вычитания округляется до 1, что меньше или равно 1/пределу:: epsilon ULP. Тем не менее математическая разница явно превышает 1.
Незначительное примечание
Выражение factor * limits::epsilon / 2
преобразует фактор в тип с плавающей точкой, что приводит к ошибкам округления для больших значений коэффициента, которые не являются точно представляемыми. Вероятно, подпрограмма не предназначена для использования с такими большими значениями (миллионы ULP в float), поэтому это должно быть указано как ограничение на рутину, а не на ошибку.
Ответ 2
Упрощение:. Вы можете избежать my_frexp, сначала отбросив не конечные случаи:
if( ! std::isfinite(a) || ! std::isfinite(b) )
return a == b;
Кажется, что isfinite находится в С++ 11 по крайней мере
EDIT Однако, если намерение состоит в том, чтобы limits::infinity()
в пределах 1 ulp limits::max()
то выше упрощения не выполняется, но не должно my_frexp() возвращать limits::max_exponent+1
в *exp
, а не max_exponent + 2?
Ответ 3
БУДУЩЕЕ ПРОФИЛИРОВАНИЕ. Если вы когда-нибудь захотите расширить такое сравнение до десятичного поплавка http://en.wikipedia.org/wiki/Decimal64_floating-point_format в будущем, и предполагая, что ldexp() и frexp() будут обрабатывать такой тип с правильным основанием, тогда выражение striclty, 0.5 in return std::copysign(0.5, num);
должно быть заменено на T(1)/limits::radix()
- или std::ldexp(T(1),-1)
или что-то в этом роде (я не смог найти удобная константа в std:: numeric_limits)
EDIT. Как заметил Немо, предположения, что 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()
of ulps * epsilon()/2
Для этого также требуется стандарт >= С++ 11