Сравнение IEEE с плавающей и двойной для равенства
Каков наилучший метод сравнения чисел с плавающей запятой IEEE и двойных чисел на равенство? Я слышал о нескольких методах, но я хотел посмотреть, что думает сообщество.
15 ответов
Я думаю, что лучший подход - это сравнивать ULP.
bool is_nan(float f)
{
return (*reinterpret_cast<unsigned __int32*>(&f) & 0x7f800000) == 0x7f800000 && (*reinterpret_cast<unsigned __int32*>(&f) & 0x007fffff) != 0;
}
bool is_finite(float f)
{
return (*reinterpret_cast<unsigned __int32*>(&f) & 0x7f800000) != 0x7f800000;
}
// if this symbol is defined, NaNs are never equal to anything (as is normal in IEEE floating point)
// if this symbol is not defined, NaNs are hugely different from regular numbers, but might be equal to each other
#define UNEQUAL_NANS 1
// if this symbol is defined, infinites are never equal to finite numbers (as they're unimaginably greater)
// if this symbol is not defined, infinities are 1 ULP away from +/- FLT_MAX
#define INFINITE_INFINITIES 1
// test whether two IEEE floats are within a specified number of representable values of each other
// This depends on the fact that IEEE floats are properly ordered when treated as signed magnitude integers
bool equal_float(float lhs, float rhs, unsigned __int32 max_ulp_difference)
{
#ifdef UNEQUAL_NANS
if(is_nan(lhs) || is_nan(rhs))
{
return false;
}
#endif
#ifdef INFINITE_INFINITIES
if((is_finite(lhs) && !is_finite(rhs)) || (!is_finite(lhs) && is_finite(rhs)))
{
return false;
}
#endif
signed __int32 left(*reinterpret_cast<signed __int32*>(&lhs));
// transform signed magnitude ints into 2s complement signed ints
if(left < 0)
{
left = 0x80000000 - left;
}
signed __int32 right(*reinterpret_cast<signed __int32*>(&rhs));
// transform signed magnitude ints into 2s complement signed ints
if(right < 0)
{
right = 0x80000000 - right;
}
if(static_cast<unsigned __int32>(std::abs(left - right)) <= max_ulp_difference)
{
return true;
}
return false;
}
Подобная техника может быть использована для парных разрядов. Хитрость заключается в том, чтобы преобразовать числа с плавающей точкой так, чтобы они были упорядочены (как целые числа), а затем просто посмотреть, насколько они различны.
Я понятия не имею, почему эта проклятая вещь портит мои подчеркивания. Изменить: О, возможно, это просто артефакт предварительного просмотра. Тогда все в порядке.
Текущая версия, которую я использую это
bool is_equals(float A, float B,
float maxRelativeError, float maxAbsoluteError)
{
if (fabs(A - B) < maxAbsoluteError)
return true;
float relativeError;
if (fabs(B) > fabs(A))
relativeError = fabs((A - B) / B);
else
relativeError = fabs((A - B) / A);
if (relativeError <= maxRelativeError)
return true;
return false;
}
Это, кажется, решает большинство проблем, сочетая относительную и абсолютную погрешность. Подход ULP лучше? Если так, то почему?
В числовом программном обеспечении вы часто хотите проверить, равны ли два числа с плавающей запятой. LAPACK полон примеров для таких случаев. Конечно, самый распространенный случай - это когда вы хотите проверить, равняется ли число с плавающей запятой "Ноль", "Один", "Два", "Половина". Если кому-то интересно, я могу выбрать некоторые алгоритмы и перейти к деталям.
Также в BLAS вы часто хотите проверить, является ли число с плавающей запятой равным нулю или единице. Например, подпрограмма dgemv может вычислять операции вида
- у = бета * у + альфа * а * х
- у = бета * у + альфа * а ^ т * х
- y = бета *y + альфа *A^H*x
Поэтому, если бета равна единице, у вас есть "плюс", а для беты - ноль "простое назначение". Таким образом, вы, безусловно, можете сократить вычислительные затраты, если уделите этим (обычным) случаям особый подход.
Конечно, вы можете разработать подпрограммы BLAS таким образом, чтобы избежать точных сравнений (например, с помощью некоторых флагов). Однако LAPACK полон примеров, когда это невозможно.
PS:
Конечно, есть много случаев, когда вы не хотите, чтобы проверка была "точно равна". Для многих это даже может быть единственным случаем, с которым им приходится иметь дело. Все, что я хочу отметить, это то, что есть и другие случаи.
Хотя LAPACK написан на Фортране, логика такая же, если вы используете другие языки программирования для числового программного обеспечения.
@DrPizza: Я не гуру производительности, но я ожидаю, что операции с фиксированной запятой будут быстрее, чем операции с плавающей запятой (в большинстве случаев).
Это скорее зависит от того, что вы делаете с ними. Тип с фиксированной точкой с тем же диапазоном, что и у плавающего элемента IEEE, будет во много раз медленнее (и во много раз больше).
Вещи, подходящие для поплавков:
3D графика, физика / инженерия, моделирование, моделирование климата....
Int позволяет мне выразить ~10^9 значений (независимо от диапазона), что кажется достаточным для любой ситуации, когда я бы хотел, чтобы два из них были равны. А если этого недостаточно, используйте 64-битную ОС, и вы получите около 10^19 различных значений.
Я действительно достиг этого предела... Я пытался совмещать время в пс и время в тактах в симуляции, где вы легко набираете 10^10 циклов. Что бы я ни делал, я очень быстро переполнял ничтожный диапазон 64-разрядных целых чисел... 10^19 - это не так много, как вы думаете, дай мне 128-битные вычисления сейчас!
Поплавки позволили мне найти решение математических проблем, так как значения были переполнены множеством нулей на нижнем уровне. Таким образом, вы в основном имели десятичное число с плавающей запятой в числе без потери точности (я хотел бы получить более ограниченное число значений, допускаемых в мантиссе с плавающей запятой, по сравнению с 64-битным целым числом, но крайне нуждающимся в этом диапазоне!).
А потом все превращается обратно в целые числа для сравнения и т. Д.
Раздражает, и в конце концов я отказался от всей попытки и просто положился на поплавки и <и>, чтобы выполнить работу. Не идеально, но работает для предполагаемого варианта использования.
О, дорогой лорд, пожалуйста, не интерпретируйте биты с плавающей точкой как целые, если вы не используете P6 или более раннюю версию.
@DrPizza: Я не гуру производительности, но я ожидаю, что операции с фиксированной запятой будут быстрее, чем операции с плавающей запятой (в большинстве случаев).
@Craig H: Конечно. Я полностью в порядке с этим, печатая это. Если a или b хранят деньги, то они должны быть представлены в фиксированной точке. Я изо всех сил пытаюсь придумать пример из реального мира, где такая логика должна быть связана с плавающими. Вещи, подходящие для поплавков:
- веса
- ряды
- расстояния
- ценности реального мира (например, из АЦП)
Для всех этих вещей, либо вы намного больше чисел и просто представляете результаты пользователю для человеческой интерпретации, либо вы делаете сравнительное утверждение (даже если такое утверждение "эта вещь находится в пределах 0,001 от этой другой вещи"). Сравнительное утверждение, подобное моему, полезно только в контексте алгоритма: часть "в пределах 0,001" зависит от того, какой физический вопрос вы задаете. Это мой 0,02. Или я должен сказать 2/100-е?
О, дорогой лорд, пожалуйста, не интерпретируйте биты с плавающей точкой как целые, если вы не используете P6 или более раннюю версию.
Даже если он заставляет его копировать из векторных регистров в целочисленные регистры через память, и даже если он останавливает конвейер, это лучший способ сделать это, с которым я когда-либо сталкивался, поскольку он обеспечивает наиболее надежные сравнения даже на первый взгляд. ошибок с плавающей запятой.
это цена, которую стоит заплатить.
Если вы ищете два одинаковых числа с плавающей точкой, то, по моему мнению, они должны быть одинаково равны. Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой подойдет вашей проблеме лучше.
Возможно, я должен объяснить проблему лучше. В C++ следующий код:
#include <iostream>
using namespace std;
int main()
{
float a = 1.0;
float b = 0.0;
for(int i=0;i<10;++i)
{
b+=0.1;
}
if(a != b)
{
cout << "Something is wrong" << endl;
}
return 1;
}
печатает фразу "Что-то не так". Вы говорите, что это должно?
Если вы ищете два одинаковых числа с плавающей точкой, то, по моему мнению, они должны быть одинаково равны. Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой подойдет вашей проблеме лучше.
Возможно, мы не можем позволить себе потерю дальности или производительности, которую может нанести такой подход.
Если у вас есть ошибки с плавающей точкой, у вас есть еще больше проблем, чем это. Хотя, думаю, это зависит от личной перспективы.
Даже если мы проведем числовой анализ, чтобы минимизировать накопление ошибки, мы не сможем устранить ее, и у нас могут остаться результаты, которые должны быть идентичными (если мы рассчитывали с реалами), но различались (потому что мы не можем рассчитывать с реалами).
Это скорее зависит от того, что вы делаете с ними. Тип с фиксированной точкой с тем же диапазоном, что и у плавающего элемента IEEE, будет во много раз медленнее (и во много раз больше).
Хорошо, но если я хочу получить бесконечно малое битовое разрешение, тогда я вернусь к исходной точке: == и!= Не имеют смысла в контексте такой проблемы.
Int позволяет мне выразить ~10^9 значений (независимо от диапазона), что кажется достаточным для любой ситуации, когда я бы хотел, чтобы два из них были равны. А если этого недостаточно, используйте 64-битную ОС, и вы получите около 10^19 различных значений.
Я могу выразить значения в диапазоне от 0 до 10^200 (например) в int, страдает только битовое разрешение (разрешение будет больше 1, но, опять же, ни у одного приложения нет такого диапазона также как такого рода резолюция).
Подводя итог, я думаю, что во всех случаях один либо представляет континуум значений, в этом случае!= И == не имеют значения, или один представляет фиксированный набор значений, которые могут быть сопоставлены с int (или другой фиксированный Тип точности).
это лучший способ сделать это из тех, что мне встречались, поскольку он обеспечивает самые надежные сравнения даже при ошибках с плавающей запятой.
Если у вас есть ошибки с плавающей точкой, у вас есть еще больше проблем, чем это. Хотя, думаю, это зависит от личной перспективы.
Если вы ищете два одинаковых числа с плавающей точкой, то, по моему мнению, они должны быть одинаково равны. Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой подойдет вашей проблеме лучше.
Это, кажется, решает большинство проблем, сочетая относительную и абсолютную погрешность. Подход ULP лучше? Если так, то почему?
ULP - это прямая мера "расстояния" между двумя числами с плавающей запятой. Это означает, что они не требуют, чтобы вы вызывали в памяти значения относительной и абсолютной ошибок, и при этом вы не должны обязательно получать эти значения "примерно правильно". С ULP вы можете напрямую указать, насколько близко вы хотите, чтобы числа были, и тот же порог работает так же хорошо для небольших значений, как и для больших.