¿Debería `Vector <float> .Equals` ser reflexivo o debería seguir la semántica IEEE 754?

9

Al comparar valores de coma flotante para igualdad, hay dos enfoques diferentes:

  • NaNno es igual a sí mismo, lo que coincide con la especificación IEEE 754 .
  • NaNser igual a sí mismo, lo que proporciona la propiedad matemática de la reflexividad que es esencial para la definición de una relación de equivalencia

Los tipos de punto flotante IEEE integrados en C # ( floaty double) siguen la semántica IEEE para ==y !=(y los operadores relacionales como <) pero aseguran la reflexividad para object.Equals, IEquatable<T>.Equals(y CompareTo).

Ahora considere una biblioteca que proporciona estructuras vectoriales sobre float/ double. Tal tipo de vector sobrecargaría ==/ !=y anularía object.Equals/ IEquatable<T>.Equals.

En lo que todos están de acuerdo es que ==/ !=debería seguir la semántica de IEEE. La pregunta es, si dicha biblioteca implementa el Equalsmétodo (que está separado de los operadores de igualdad) de una manera reflexiva o que coincida con la semántica IEEE.

Argumentos para usar la semántica IEEE para Equals:

  • Sigue a IEEE 754
  • Es (posiblemente mucho) más rápido porque puede aprovechar las instrucciones SIMD

    He hecho una pregunta por separado sobre stackoverflow sobre cómo expresaría la igualdad reflexiva utilizando las instrucciones SIMD y su impacto en el rendimiento: instrucciones SIMD para la comparación de igualdad de punto flotante

    Actualización: Parece que es posible implementar la igualdad reflexiva de manera eficiente utilizando tres instrucciones SIMD.

  • La documentación para Equalsno requiere reflexividad cuando involucra punto flotante:

    Las siguientes declaraciones deben ser ciertas para todas las implementaciones del método Equals (Object). En la lista, x, y, y zrepresentan referencias a objetos que no son nulos.

    x.Equals(x)retornos true, excepto en casos que involucran tipos de punto flotante. Ver ISO / IEC / IEEE 60559: 2011, Tecnología de la información - Sistemas de microprocesador - Aritmética de punto flotante.

  • Si usa flotadores como teclas de diccionario, está viviendo en un estado de pecado y no debe esperar un comportamiento sensato.

Argumentos para ser reflexivo:

  • Es compatible con los tipos existentes, incluyendo Single, Double, Tupley System.Numerics.Complex.

    No conozco ningún precedente en el BCL donde Equalssiga IEEE en lugar de ser reflexivo. Contraejemplos incluyen Single, Double, Tupley System.Numerics.Complex.

  • Equalses utilizado principalmente por contenedores y algoritmos de búsqueda que dependen de la reflexividad. Para estos algoritmos, una ganancia de rendimiento es irrelevante si les impide funcionar. No sacrifique la corrección por el rendimiento.
  • Rompe todos los conjuntos y diccionarios en base de hash, Contains, Find, IndexOfen varias colecciones / LINQ, LINQ operaciones de conjunto basada ( Union, Except, etc.) si los datos contienen NaNvalores.
  • El código que realiza cálculos reales donde IEEE semántico es aceptable generalmente funciona en tipos y usos concretos ==/ !=(o más probablemente en comparaciones épsilon).

    Actualmente no puede escribir cálculos de alto rendimiento utilizando genéricos, ya que necesita operaciones aritméticas para eso, pero estos no están disponibles a través de interfaces / métodos virtuales.

    Por lo tanto, un Equalsmétodo más lento no afectaría a la mayoría de los códigos de alto rendimiento.

  • Es posible proporcionar un IeeeEqualsmétodo o IeeeEqualityComparer<T>para los casos en los que necesita la semántica IEEE o necesita una ventaja de rendimiento.

En mi opinión, estos argumentos favorecen fuertemente una implementación reflexiva.

El equipo CoreFX de Microsoft planea introducir dicho tipo de vector en .NET. A diferencia de mí , prefieren la solución IEEE , principalmente debido a las ventajas de rendimiento. Dado que tal decisión ciertamente no cambiará después de un lanzamiento final, quiero recibir comentarios de la comunidad, sobre lo que creo que es un gran error.

CodesInChaos
fuente
1
Excelente y sugerente pregunta. Para mí (al menos), no tiene sentido ==y Equalsdevolvería resultados diferentes. Muchos programadores asumen que lo son y hacen lo mismo . Además, en general, las implementaciones de los operadores de igualdad invocan el Equalsmétodo. Usted ha argumentado que uno podría incluir a IeeeEquals, pero también podría hacerlo al revés e incluir un ReflexiveEqualsmétodo-. El Vector<float>tipo puede usarse en muchas aplicaciones críticas de rendimiento y debe optimizarse en consecuencia.
die maus
@diemaus Algunas razones por las que no encuentro eso convincente: 1) para float/ doubley varios otros tipos, ==y Equalsya son diferentes. Creo que una inconsistencia con los tipos existentes sería aún más confusa que la inconsistencia entre ==y Equalsaún tendrá que lidiar con otros tipos. 2) Casi todos los algoritmos / colecciones genéricos usan Equalsy dependen de su reflexividad para funcionar (LINQ y diccionarios), mientras que los algoritmos concretos de punto flotante generalmente usan de ==dónde obtienen su semántica IEEE.
CodesInChaos
Consideraría Vector<float>una "bestia" diferente a una simple floato double. Según esa medida, no puedo ver la razón Equalso el ==operador para cumplir con los estándares de ellos. Usted mismo dijo: "Si está usando flotadores como teclas del diccionario, está viviendo en un estado de pecado y no debe esperar un comportamiento sensato". Si uno fuera a almacenar NaNen un diccionario, entonces es su propia condenada culpa por usar una práctica terrible. No creo que el equipo CoreFX no haya pensado en esto. Yo iría con uno ReflexiveEqualso similar, solo por el bien del rendimiento.
die maus

Respuestas:

5

Yo diría que el comportamiento de IEEE es correcto. NaNs no son equivalentes entre sí de ninguna manera; corresponden a condiciones mal definidas donde una respuesta numérica no es apropiada.

Más allá de los beneficios de rendimiento que provienen del uso de la aritmética IEEE que la mayoría de los procesadores admiten de forma nativa, creo que hay un problema semántico al decir que si isnan(x) && isnan(y), entonces x == y. Por ejemplo:

// C++
double inf = std::numeric_limits<double>::infinity();
double x = 0.0 / 0.0;
double y = inf - inf;

Yo diría que no hay una razón significativa por la que uno consideraría xigual a y. Difícilmente podría concluir que son números equivalentes; no son números en absoluto, por lo que parece un concepto completamente inválido.

Además, desde una perspectiva de diseño de API, si está trabajando en una biblioteca de propósito general que está destinada a ser utilizada por muchos programadores, tiene sentido usar la semántica de punto flotante más típica de la industria. El objetivo de una buena biblioteca es ahorrar tiempo para quienes la usan, por lo que construir un comportamiento no estándar está listo para la confusión.

Jason R
fuente
3
Eso NaN == NaNdebería devolver falso es indiscutible. La pregunta es qué .Equalsdebe hacer el método. Por ejemplo, si lo uso NaNcomo clave de diccionario, el valor asociado se vuelve irrecuperable si NaN.Equals(NaN)devuelve falso.
CodesInChaos
1
Creo que hay que optimizar para el caso común. El caso común para un vector de números es el cálculo numérico de alto rendimiento (a menudo optimizado con instrucciones SIMD). Yo diría que usar un vector como clave de diccionario es un caso de uso extremadamente raro, y difícilmente vale la pena diseñar su semántica. El contra-argumento que parece más razonable para mí es consistencia, ya que los existentes Single, Doubleclases, etc. ya tienen el comportamiento reflexivo. En mi humilde opinión, esa fue la decisión equivocada para empezar. Pero no dejaría que la elegancia se interpusiera en la utilidad / velocidad.
Jason R
Pero los cálculos numéricos generalmente usarán lo ==que siempre ha seguido a IEEE, por lo que obtendrían el código rápido sin importar cómo Equalsse implemente. En mi opinión, todo el punto de tener un Equalsmétodo separado es usar algoritmos que no se preocupan por el tipo concreto, como la Distinct()función de LINQ .
CodesInChaos
1
Lo entiendo. Pero argumentaría en contra de una API que tiene un ==operador y una Equals()función que tienen una semántica diferente. Creo que está pagando un costo de confusión potencial desde la perspectiva del desarrollador, sin ningún beneficio real (no asigno ningún valor para poder usar un vector de números como clave de diccionario). Es solo mi opinión; No creo que haya una respuesta objetiva a la pregunta en cuestión.
Jason R
0

Hay un problema: IEEE754 define las operaciones relacionales y la igualdad de una manera adecuada para aplicaciones numéricas. No es adecuado para la clasificación y el hash. Entonces, si desea ordenar una matriz basada en valores numéricos, o si desea agregar valores numéricos a un conjunto o usarlos como claves en un diccionario, declara que los valores NaN no están permitidos o no usa IEEE754 operaciones integradas. Su tabla hash tendría que asegurarse de que todos los NaN coincidan con el mismo valor y compararlos entre sí.

Si define Vector, debe tomar la decisión de diseño si desea usarlo solo con fines numéricos o si debe ser compatible con la clasificación y el hash. Personalmente, creo que el propósito numérico debería ser mucho más importante. Si se necesita la clasificación / hashing, puede escribir una clase con Vector como miembro y definir el hashing y la igualdad en esa clase de la manera que desee.

gnasher729
fuente
1
Estoy de acuerdo en que los propósitos numéricos son más importantes. Pero ya tenemos los operadores ==y !=para ellos. En mi experiencia, el Equalsmétodo es utilizado solo por algoritmos no numéricos.
CodesInChaos