Comparando IEEE flota y dobla para la igualdad

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

El mejor enfoque, creo, es comparar las ULP .

bool is_nan(float f) { return (*reinterpret_cast(&f) & 0x7f800000) == 0x7f800000 && (*reinterpret_cast(&f) & 0x007fffff) != 0; } bool is_finite(float f) { return (*reinterpret_cast(&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(&lhs)); // transform signed magnitude ints into 2s complement signed ints if(left < 0) { left = 0x80000000 - left; } signed __int32 right(*reinterpret_cast(&rhs)); // transform signed magnitude ints into 2s complement signed ints if(right < 0) { right = 0x80000000 - right; } if(static_cast(std::abs(left - right)) <= max_ulp_difference) { return true; } return false; } 

Una técnica similar se puede usar para dobles. El truco es convertir los flotadores para que estén ordenados (como enteros) y luego ver qué tan diferentes son.

No tengo idea de por qué esta maldita cosa está arruinando mis guiones bajos. Editar: Oh, tal vez eso es solo un artefacto de la vista previa. Eso está bien entonces.

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 resolver la mayoría de los problemas combinando tolerancia de error absoluta y relativa. ¿El enfoque de ULP es mejor? Si es así, ¿por qué?

@DrPizza: No soy un gurú del rendimiento, pero espero que las operaciones de punto fijo sean más rápidas que las operaciones de coma 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 carrozas:

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

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

También en BLAS a menudo quiere comprobar si un número de punto 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 más” y para beta equivale a cero una “asignación simple”. Entonces, ciertamente puede reducir el costo computacional si le da a estos casos (comunes) un tratamiento especial.

Claro, podría diseñar las rutinas BLAS de tal manera que pueda evitar comparaciones exactas (por ejemplo, utilizando algunos indicadores). Sin embargo, el LAPACK está lleno de ejemplos donde no es posible.

PD:

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

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

Oh, querido señor, por favor no interpretes los bits del flotador como si fueses a menos que estés ejecutando un P6 o anterior.

Incluso si hace que copie de los registros vectoriales a los registros enteros a través de la memoria, e incluso si bloquea 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 los errores de coma flotante.

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

Esto parece resolver la mayoría de los problemas combinando tolerancia de error absoluta y relativa. ¿El enfoque de ULP es mejor? 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 invoque los valores de error relativos y absolutos, ni tiene que asegurarse de obtener esos valores “sobre la derecha”. Con los ULP, puede express directamente qué tan cerca quiere que sean los números, y el mismo umbral funciona igual de bien para los valores pequeños que para los grandes.

Si tiene errores de coma flotante, tiene incluso más problemas que esto. Aunque supongo que eso depende de la perspectiva personal.

Incluso si hacemos el análisis numérico para minimizar la acumulación de errores, 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 buscas dos flotadores para ser iguales, entonces deberían ser idénticos en mi opinión. Si se enfrenta a un problema de redondeo de punto flotante, tal vez una representación de punto fijo se adapte mejor a su problema.

Si buscas dos flotadores para ser iguales, entonces deberían ser idénticos en mi opinión. Si se enfrenta a un problema de redondeo de punto flotante, tal vez una representación de punto fijo se adapte mejor a su problema.

Tal vez no podemos permitirnos la pérdida de rango o rendimiento que tal enfoque infligiría.

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

@ Craig H: Claro. Estoy totalmente de acuerdo con que imprima eso. Si a o b almacena dinero, deberían estar representados en un punto fijo. Estoy luchando por pensar en un ejemplo del mundo real donde tal lógica deba estar aliada a las carrozas. Cosas adecuadas para carrozas:

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

Para todas estas cosas, o usted mucho entonces números y simplemente presenta los resultados al usuario para la interpretación humana, o hace una statement comparativa (incluso si tal afirmación es, “esto está dentro de 0.001 de esta otra cosa”). Una afirmación comparativa como la mía solo es útil en el contexto del algoritmo: la parte “dentro de 0.001” depende de la pregunta física que haga. Eso es mi 0.02. ¿O debería decir 2 / 100ths?

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).

Está bien, pero si quiero una resolución de bits infinitesimalmente pequeña, volveré a mi punto original: == y! = No tienen sentido en el contexto de un problema así.

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

Puedo express valores de un rango de 0 a 10 ^ 200 (por ejemplo) en un int, es solo la resolución de bit lo que sufre (la resolución sería mayor que 1, pero, de nuevo, 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 pueden correlacionarse con un int (o con otro fijo) -precisión tipo).

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

De hecho, llegué a ese límite … Estaba tratando de hacer malabares con los tiempos en ps y el tiempo en ciclos de reloj en una simulación en la que tocas fácilmente 10 ^ 10 ciclos. No importa lo que hice, muy rápidamente rebasé el pequeño rango de enteros de 64 bits … 10 ^ 19 no es tanto como crees, ¡dame 128 bits de computación ahora!

Flota me permitió obtener una solución a los problemas matemáticos, ya que los valores se desbordaron con ceros de lotes en el extremo inferior. Así que básicamente tenía un punto decimal flotante aronud en el número sin pérdida de precisión (me gustaría con el número más limitado de valores permitidos en la mantisa de un flotador en comparación con un int de 64 bits, ¡pero necesitaba desesperadamente este rango! )

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

Molesto, y al final eliminé todo el bash y solo confié en flotadores y para hacer el trabajo. No es perfecto, pero funciona para el caso de uso previsto.

Si buscas dos flotadores para ser iguales, entonces deberían ser idénticos en mi opinión. Si se enfrenta a un problema de redondeo de punto flotante, tal vez una representación de punto fijo se adapte mejor a su problema.

Tal vez debería explicar el problema mejor. En C ++, el siguiente código:

 #include  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 está mal". ¿Estás diciendo que debería?

Oh, querido señor, por favor no interpretes los bits del flotador como si fueses a menos que estés ejecutando un P6 o anterior.

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

Si tiene errores de coma flotante, tiene incluso más problemas que esto. Aunque supongo que eso depende de la perspectiva personal.