Pregunta

¿Cuál es el mejor método para comparar flotantes y dobles de IEEE en busca de igualdad?He oído hablar de varios métodos, pero quería ver qué pensaba la comunidad.

¿Fue útil?

Solución

Creo que el mejor enfoque es comparar 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;
}

Se puede utilizar una técnica similar para dobles.El truco consiste en convertir los flotantes para que estén ordenados (como si fueran números enteros) y luego ver cuán diferentes son.

No tengo idea de por qué esta maldita cosa está arruinando mis guiones bajos.Editar:Oh, tal vez eso sea sólo un artefacto de la vista previa.Entonces está bien.

Otros consejos

La versión actual que estoy usando es esta.

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;
}

Esto parece solucionar la mayoría de los problemas al combinar la tolerancia a errores relativa y absoluta.¿Es mejor el enfoque ULP?Si es así, ¿por qué?

@DrPizza:No soy un gurú del rendimiento, pero esperaría que las operaciones de punto fijo sean más rápidas que las de punto flotante (en la mayoría de los casos).

Más bien depende de lo que estés haciendo con ellos.Un tipo de punto fijo con el mismo rango que un flotador IEEE sería muchas veces más lento (y muchas veces más grande).

Cosas adecuadas para flotadores:

Gráficos 3D, física/ingeniería, simulación, simulación climática....

En el software numérico a menudo desea probar si dos números de punto flotante son exactamente igual.LAPACK está lleno de ejemplos para tales casos.Claro, el caso más común es cuando desea probar si un número de coma flotante es igual a "Cero", "Uno", "Dos", "Mitad".Si alguien está interesado, puedo elegir algunos algoritmos y entrar en más detalles.

Además, en BLAS a menudo se desea comprobar si un número de coma flotante es exactamente cero o uno.Por ejemplo, la rutina dgemv puede calcular operaciones de la forma

  • y = beta*y + alfa*A*x
  • y = beta*y + alfa*A^T*x
  • y = beta*y + alfa*A^H*x

Entonces, si beta es igual a Uno, tiene una "asignación positiva" y si beta es igual a Cero, una "asignación simple".Por lo tanto, ciertamente puede reducir el costo computacional si les da a estos casos (comunes) un tratamiento especial.

Claro, podría diseñar las rutinas BLAS de tal manera que pueda evitar comparaciones exactas (p. ej.usando algunas banderas).Sin embargo, LAPACK está lleno de ejemplos en los que esto no es posible.

PD.:

  • Ciertamente, hay muchos casos en los que no desea verificar "es exactamente igual".Para muchas personas, este podría incluso ser el único caso con el que tengan que lidiar.Lo único que quiero señalar es que también hay otros casos.

  • Aunque LAPACK está escrito en Fortran, la lógica es la misma si utiliza otros lenguajes de programación para software numérico.

Dios mío, por favor no interpretes los bits flotantes como enteros a menos que estés ejecutando un P6 o una versión anterior.

Incluso si hace que se copie de registros vectoriales a registros enteros a través de la memoria, e incluso si detiene la canalización, es la mejor manera de hacerlo que he encontrado, en la medida en que proporciona las comparaciones más sólidas incluso en la cara. de errores de coma flotante.

es decir.es un precio que vale la pena pagar.

Esto parece solucionar la mayoría de los problemas al combinar la tolerancia a errores relativa y absoluta.¿Es mejor el enfoque ULP?Si es así, ¿por qué?

Los ULP son una medida directa de la "distancia" entre dos números de coma flotante.Esto significa que no requieren que usted evoque los valores de error relativo y absoluto, ni tiene que asegurarse de obtener esos valores "más o menos correctos".Con los ULP, puede expresar directamente qué tan cerca desea que estén los números, y el mismo umbral funciona igual de bien para valores pequeños como grandes.

Si tiene errores de punto flotante, tendrá aún más problemas que este.Aunque supongo que eso depende de una perspectiva personal.

Incluso si hacemos el análisis numérico para minimizar la acumulación de error, no podemos eliminarlo y podemos quedarnos con resultados que deberían ser idénticos (si estuviéramos calculando con reales) pero diferentes (porque no podemos calcular con reales).

Si está buscando que dos flotadores sean iguales, entonces, en mi opinión, deberían ser idénticamente iguales.Si se enfrenta a un problema de redondeo en punto flotante, quizás una representación en punto fijo se adapte mejor a su problema.

Si está buscando que dos flotadores sean iguales, entonces, en mi opinión, deberían ser idénticamente iguales.Si se enfrenta a un problema de redondeo en punto flotante, quizás una representación en punto fijo se adapte mejor a su problema.

Quizás no podamos permitirnos la pérdida de alcance o rendimiento que tal enfoque infligiría.

@DrPizza:No soy un gurú del rendimiento, pero esperaría que las operaciones de punto fijo sean más rápidas que las de punto flotante (en la mayoría de los casos).

@Craig H:Seguro.Estoy totalmente de acuerdo con que imprima eso.Si a o b almacenan dinero, entonces deben representarse en punto fijo.Me cuesta pensar en un ejemplo del mundo real en el que esa lógica debería combinarse con las carrozas.Cosas adecuadas para flotadores:

  • pesas
  • rangos
  • distancias
  • valores del mundo real (como los de un ADC)

Para todas estas cosas, o usted calcula los números y simplemente presenta los resultados al usuario para su interpretación humana, o hace una declaración comparativa (incluso si dicha declaración es "esta cosa está dentro de 0,001 de esta otra cosa").Una afirmación comparativa como la mía sólo es útil en el contexto del algoritmo:la parte "dentro de 0,001" depende de lo que físico pregunta que estás haciendo.Ese mi 0.02.¿O debería decir 2/100?

Más bien depende de lo que esté haciendo con ellos.Un tipo de punto fijo con el mismo rango que un flotador IEEE sería muchas veces más lento (y muchas veces más grande).

Bien, pero si quiero una resolución de bits infinitamente pequeña, entonces vuelvo a mi punto original:== y != no tienen significado en el contexto de tal problema.

Un int me permite expresar valores de ~10^9 (independientemente del rango), lo que parece suficiente para cualquier situación en la que me importe que dos de ellos sean iguales.Y si eso no es suficiente, use un sistema operativo de 64 bits y tendrá alrededor de 10^19 valores distintos.

Puedo expresar valores en un rango de 0 a 10^200 (por ejemplo) en un int, es solo la resolución de bits la que sufre (la resolución sería mayor que 1, pero, nuevamente, ninguna aplicación tiene ese tipo de rango también como ese tipo de resolución).

Para resumir, creo que en todos los casos uno representa un continuo de valores, en cuyo caso != y == son irrelevantes, o uno representa un conjunto fijo de valores, que se puede asignar a un int (u otro fijo -tipo de precisión).

Un int me permite expresar ~ 10^9 valores (independientemente del rango) que parece suficiente para cualquier situación en la que me importen que dos de ellos sean iguales.Y si eso no es suficiente, use un sistema operativo de 64 bits y obtendrá alrededor de 10^19 valores distintos.

De hecho, he llegado a ese límite...Estaba tratando de hacer malabarismos con los tiempos en ps y el tiempo en ciclos de reloj en una simulación en la que fácilmente alcanzas 10^10 ciclos.No importa lo que hice, rápidamente desbordé el insignificante rango de enteros de 64 bits...10^19 no es tanto como crees, ¡dame computación de 128 bits ahora!

Los flotadores me permitieron obtener una solución a los problemas matemáticos, ya que los valores se desbordaban con muchos ceros en el extremo inferior.Básicamente, tenías un punto decimal flotando alrededor del número sin pérdida de precisión (me gustaría tener un número distinto más limitado de valores permitidos en la mantisa de un flotante en comparación con un int de 64 bits, ¡pero necesitaba desesperadamente el rango! ).

Y luego las cosas se vuelven a convertir a números enteros para comparar, etc.

Molesto, y al final descarté todo el intento y solo confié en los flotadores y < y > para realizar el trabajo.No es perfecto, pero funciona para el caso de uso previsto.

Si está buscando que dos flotadores sean iguales, entonces, en mi opinión, deberían ser idénticamente iguales.Si se enfrenta a un problema de redondeo en punto flotante, quizás una representación en punto fijo se adapte mejor a su problema.

Quizás debería explicar mejor el problema.En C++, el siguiente código:

#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;
}

imprime la frase "Algo anda mal".¿Estás diciendo que debería?

Dios mío, por favor no interpretes los bits flotantes como enteros a menos que estés ejecutando un P6 o una versión anterior.

es la mejor manera de hacerlo que he encontrado, en la medida en que proporciona las comparaciones más sólidas incluso frente a errores de punto flotante.

Si tiene errores de punto flotante, tendrá aún más problemas que este.Aunque supongo que eso depende de una perspectiva personal.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top