Пересмотрено сравнение с плавающей точкой

Эта тема много раз поднималась в StackOverflow, но я считаю, что это новый подход. Да я прочитал Статьи Брюса Доусона а также Что каждый компьютерщик должен знать об арифметике с плавающей точкой а также этот хороший ответ.

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

  1. Вычисления с плавающей точкой не точны
  2. Будь то a-b «маленький» зависит от масштаба a а также b
  3. Будь то a-b «маленький» зависит от типа a а также b (например, float, double, long double)
  4. Плавающая точка обычно имеет + -infinity, NaN и денормализованные представления, любое из которых может мешать наивной формулировке

Этот ответ — ака. «Подход Google» — похоже, популярен. Он обрабатывает все сложные случаи. И это очень точно масштабирует сравнение, проверяя, находятся ли два значения в пределах фиксированного числа ULPs друг друга. Так, например, очень большое число сравнивается «почти равным» бесконечности.

Тем не мение:

  • Это очень грязно, на мой взгляд.
  • Он не особенно переносим, ​​сильно полагается на внутренние представления, использует объединение для чтения битов из числа с плавающей запятой и т. Д.
  • Он работает только с IEEE 754 одинарной и двойной точности (в частности, без двойной x86 long)

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

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

26

Решение

«Почти равный» не является хорошей функцией

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

21

Другие решения

Упрощение: Вы могли бы избежать 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?

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

0
По вопросам рекламы [email protected]