Сравнение чисел с плавающей запятой и двойных чисел IEEE на предмет равенства

StackOverflow https://stackoverflow.com/questions/21265

Вопрос

Каков наилучший метод сравнения чисел с плавающей запятой и двойных чисел IEEE на предмет равенства?Я слышал о нескольких методах, но мне хотелось узнать, что думает сообщество.

Это было полезно?

Решение

Я считаю, что лучший подход — это сравнить 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 лучше?Если да, то почему?

@DrPizza:Я не гуру производительности, но я ожидаю, что операции с фиксированной запятой будут быстрее, чем операции с плавающей запятой (в большинстве случаев).

Это скорее зависит от того, что вы с ними делаете.Тип с фиксированной запятой с тем же диапазоном, что и число с плавающей запятой IEEE, будет во много раз медленнее (и во много раз больше).

Вещи, подходящие для поплавков:

3D-графика, физика/инженерия, моделирование, моделирование климата....

В числовом программном обеспечении часто требуется проверить, являются ли два числа с плавающей запятой точно равный.В LAPACK полно примеров для таких случаев.Конечно, наиболее распространенным случаем является проверка того, равно ли число с плавающей запятой «Нулю», «Одину», «Двум», «Половине».Если кому-то интересно, я могу выбрать несколько алгоритмов и рассказать подробнее.

Также в BLAS часто требуется проверить, равно ли число с плавающей запятой нулю или единице.Например, подпрограмма dgemv может вычислять операции вида

  • y = бета*y + альфа*A*x
  • y = бета*y + альфа*A^T*x
  • y = бета*y + альфа*A^H*x

Таким образом, если бета равна единице, у вас есть «плюсовое присвоение», а для бета равной нулю — «простое присвоение».Таким образом, вы, безусловно, можете сократить вычислительные затраты, если уделите этим (обычным) случаям особый подход.

Конечно, вы можете спроектировать процедуры BLAS таким образом, чтобы избежать точных сравнений (например,используя некоторые флаги).Однако в LAPACK полно примеров, когда это невозможно.

P.S.:

  • Конечно, во многих случаях вам не нужна проверка «точно равно».Для многих людей это даже может быть единственным случаем, с которым им когда-либо приходилось иметь дело.Единственное, что я хочу отметить, это то, что есть и другие случаи.

  • Хотя LAPACK написан на Фортране, логика остается той же, если вы используете другие языки программирования для числового программного обеспечения.

Господи, пожалуйста, не интерпретируйте биты с плавающей запятой как целые числа, если только вы не работаете на P6 или более ранней версии.

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

то естьэто цена, которую стоит заплатить.

Кажется, это решает большинство проблем, сочетая относительную и абсолютную устойчивость к ошибкам.Чем подход ULP лучше?Если да, то почему?

ULP — это прямая мера «расстояния» между двумя числами с плавающей запятой.Это означает, что они не требуют от вас вызывать в воображении относительные и абсолютные значения ошибок, а также вам не нужно следить за тем, чтобы эти значения были «почти правильными».С помощью ULP вы можете напрямую выразить, насколько близкими должны быть числа, и один и тот же порог работает одинаково хорошо как для малых значений, так и для больших.

Если у вас есть ошибки с плавающей запятой, у вас еще больше проблем.Хотя я думаю, это зависит от личной точки зрения.

Даже если мы проведем численный анализ, чтобы минимизировать накопление ошибок, мы не сможем устранить их и можем остаться с результатами, которые должны быть идентичными (если бы мы рассчитывали с реальными числами), но отличаться (потому что мы не можем рассчитывать с реальными числами).

Если вы хотите, чтобы два числа с плавающей запятой были равны, то, по моему мнению, они должны быть одинаково равными.Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой лучше подойдет для вашей проблемы.

Если вы хотите, чтобы два числа с плавающей запятой были равны, то, по моему мнению, они должны быть одинаково равными.Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой лучше подойдет для вашей проблемы.

Возможно, мы не можем позволить себе потерю дальности или производительности, которую может вызвать такой подход.

@DrPizza:Я не гуру производительности, но я ожидаю, что операции с фиксированной запятой будут быстрее, чем операции с плавающей запятой (в большинстве случаев).

@Крэйг Х:Конечно.Я полностью согласен с тем, чтобы это распечатать.Если a или b хранят деньги, то они должны быть представлены в фиксированной точке.Я изо всех сил пытаюсь придумать реальный пример, где такая логика могла бы быть связана с числами с плавающей запятой.Вещи, подходящие для поплавков:

  • гири
  • ранги
  • расстояния
  • реальные мировые значения (например, от АЦП)

Для всех этих вещей либо вы затем сильно подсчитываете и просто представляете результаты пользователю для человеческой интерпретации, либо делаете сравнительное утверждение (даже если такое утверждение гласит: «Эта вещь находится в пределах 0,001 от этой другой вещи»).Сравнительное утверждение, подобное моему, полезно только в контексте алгоритма:часть «в пределах 0,001» зависит от того, что физический вопрос, который вы задаете.Это мой 0,02.Или лучше сказать 2/100?

Это скорее зависит от того, что вы делаете с ними.Тип фиксированной точки с тем же диапазоном, что и поплавок IEEE, был бы много раз медленнее (и во много раз больше).

Хорошо, но если мне нужно бесконечно малое битовое разрешение, то я возвращаюсь к исходной точке:== и != не имеют смысла в контексте такой проблемы.

Int позволяет мне выражать ~10^9 значений (независимо от диапазона), чего кажется достаточным для любой ситуации, когда мне важно, чтобы два из них были равны.А если этого недостаточно, используйте 64-битную ОС, и вы получите около 10^19 различных значений.

Я могу выразить значения в диапазоне от 0 до 10^200 (например) в виде int, страдает только битовое разрешение (разрешение будет больше 1, но, опять же, ни одно приложение не имеет такого диапазона). как такое разрешение).

Подводя итог, я думаю, что во всех случаях либо один представляет континуум значений, и в этом случае != и == не имеют значения, либо один представляет фиксированный набор значений, который может быть сопоставлен с целым числом (или другим фиксированным -прецизионный тип).

Int позволяет мне выразить значения ~ 10^9 (независимо от диапазона), что кажется достаточно для любой ситуации, когда я бы заботился о том, чтобы два из них были равны.И если этого недостаточно, используйте 64-битную ОС, и у вас есть около 10^19 различных значений.

Я действительно достиг этого предела...Я пытался жонглировать временем в пс и временем в тактовых циклах в симуляции, где вы легко достигаете 10 ^ 10 циклов.Что бы я ни делал, я очень быстро переполнил жалкий диапазон 64-битных целых чисел...10^19 — это не так много, как вы думаете, дайте мне 128-битные вычисления прямо сейчас!

Плавающие числа позволили мне найти решение математических проблем, поскольку значения в нижней части переполнялись нулями лотов.Таким образом, у вас в основном была десятичная точка, плавающая в числе без потери точности (мне хотелось бы, чтобы в мантиссе числа с плавающей запятой было более ограниченное число значений по сравнению с 64-битным целым числом, но мне крайне нужен этот диапазон! ).

А затем все преобразуется обратно в целые числа для сравнения и т. д.

Это раздражало, и в конце концов я отказался от всей этой попытки и просто полагался на плавающие числа и < и >, чтобы выполнить работу.Не идеально, но работает для предусмотренного варианта использования.

Если вы хотите, чтобы два числа с плавающей запятой были равны, то, по моему мнению, они должны быть одинаково равными.Если вы столкнулись с проблемой округления с плавающей запятой, возможно, представление с фиксированной запятой лучше подойдет для вашей проблемы.

Возможно, мне следует лучше объяснить проблему.В 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;
}

печатает фразу «Что-то не так».Вы хотите сказать, что так и должно быть?

Господи, пожалуйста, не интерпретируйте биты с плавающей запятой как целые числа, если только вы не работаете на P6 или более ранней версии.

это лучший способ сделать это, с которым я столкнулся, поскольку он обеспечивает наиболее надежные сравнения даже при наличии ошибок с плавающей запятой.

Если у вас есть ошибки с плавающей запятой, у вас еще больше проблем.Хотя я думаю, это зависит от личной точки зрения.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top