Сравнение IEEE float и double на равенство

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


person Craig H    schedule 21.08.2008    source источник
comment
Ознакомьтесь с этим ответом на аналогичный вопрос.   -  person grom    schedule 22.08.2008


Ответы (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;
}

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

Понятия не имею, почему эта чертова штука портит мне подчеркивание. Изменить: О, возможно, это просто артефакт предварительного просмотра. Тогда это нормально.

person DrPizza    schedule 21.08.2008
comment
Это бесспорно выигрывает в отношении точности. Но для производительности ... вам придется немного пожертвовать точностью ради улучшения скорости. Согласованный? - person OJ.; 08.10.2008
comment
Если вам нужны двойники или переносимость: я нашел отличную кроссплатформенную реализацию этого, которая может работать как с двойными, так и с плавающими точками в Google Test, и разместил ее здесь: stackoverflow.com/questions/17333/ - person skrebbel; 06.08.2010

Текущая версия, которую я использую, это

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 лучше? Если да, то почему?

person Craig H    schedule 21.08.2008

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

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

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

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

person DrPizza    schedule 21.08.2008

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

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

  • у = бета * у + альфа * А * х
  • у = бета * у + альфа * А ^ Т * х
  • y = бета * y + альфа * A ^ H * x

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

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

P.S.:

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

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

person Michael Lehn    schedule 10.06.2012

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

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

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

person DrPizza    schedule 21.08.2008
comment
Я подозреваю, что (a==b || (a!=a && b != b)) на многих процессорах будет быстрее, чем что-либо, связанное с преобразованием битов с плавающей запятой в целое число, хотя мне действительно больно, что выражения, подобные приведенным выше, необходимы. Интересно, насколько выгодно решение Nan! = NaN и сколько оно стоит потраченного впустую кода и времени на отладку? - person supercat; 04.04.2015

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

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

person DrPizza    schedule 21.08.2008

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

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

person DrPizza    schedule 21.08.2008

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

person Nick    schedule 21.08.2008
comment
работа с (почти) равенством - это частые случаи в циклах, когда вы хотите остановиться, когда все становится достаточно близко, но когда вы не ожидаете, что они когда-либо точно сойдутся. - person BCS; 10.12.2008

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

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

person DrPizza    schedule 21.08.2008

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

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

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

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

person Nick    schedule 21.08.2008

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

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

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

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

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

person Nick    schedule 21.08.2008

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

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

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

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

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

person jakobengblom2    schedule 09.10.2008

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

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

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

person Craig H    schedule 21.08.2008
comment
Вы почти всегда получите a! = B из-за точности с плавающей запятой и ошибок округления. - person Jim Kramer; 01.06.2009

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

person Mat Noguchi    schedule 21.08.2008

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

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

person Mat Noguchi    schedule 21.08.2008