Si copio un flotante a otra variable, ¿serán iguales?

167

Sé que usar ==para verificar la igualdad de las variables de punto flotante no es una buena manera. Pero solo quiero saber eso con las siguientes declaraciones:

float x = ...

float y = x;

assert(y == x)

Como yse copia de x, ¿será cierta la afirmación?

Wei Li
fuente
78
Permítanme proporcionar una recompensa de 50 a alguien que realmente demuestre la desigualdad mediante una demostración con código real. Quiero ver la cosa de 80 vs 64 bits en acción. Además de otros 50 para obtener una explicación del código ensamblador generado que muestra que una variable está en un registro y la otra no (o cualquiera que sea la razón de la desigualdad, me gustaría que se explicara en un nivel bajo).
Thomas Weller
1
@ThomasWeller el error de GCC sobre esto: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; sin embargo, acabo de intentar reproducirlo en un sistema x86-64 y no lo hace, incluso con -ffast-math. Sospecho que necesita un viejo GCC en un sistema de 32 bits.
pjc50
55
@ pjc50: en realidad necesita un sistema de 80 bits para reproducir el error 323; Es la FPU 80x87 la que causó el problema. x86-64 usa la FPU SSE. Los bits adicionales causan el problema, ya que se redondean al derramar un valor en un flotante de 32 bits.
MSalters
44
Si la teoría de MSalters es correcta (y sospecho que lo es), puede reprobar compilando para 32 bits ( -m32) o instruyendo a GCC para que use la FPU x87 ( -mfpmath=387).
Cody Gray
44
Cambie "48 bits" a "80 bits", y luego puede eliminar el adjetivo "mítico" allí, @Hot. Eso es precisamente lo que se discutió inmediatamente antes de su comentario. El x87 (FPU para arquitectura x86) utiliza registros de 80 bits, un formato de "precisión extendida".
Cody Gray

Respuestas:

125

Además del assert(NaN==NaN);caso señalado por kmdreko, puede tener situaciones con x87-math, cuando los flotantes de 80 bits se almacenan temporalmente en la memoria y luego se comparan con los valores que aún se almacenan dentro de un registro.

Posible ejemplo mínimo, que falla con gcc9.2 cuando se compila con -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Demostración de Godbolt: https://godbolt.org/z/X-Xt4R

El volatileprobablemente se puede omitir, si logra crear suficientes registro a la presión de haber yalmacenado y vuelve a cargar desde la memoria (pero lo suficientemente confundir al compilador, no omitir la comparación todos juntos).

Consulte la referencia de preguntas frecuentes de GCC:

chtz
fuente
2
Parece extraño que se consideren los bits adicionales al comparar una floatprecisión estándar con una precisión adicional.
Nat
13
@Nat Se es extraño; Esto es un error .
Carreras ligeras en órbita el
13
@ThomasWeller No, es un premio razonable. Aunque me gustaría que la respuesta señalara que este es un comportamiento no conforme
carreras de ligereza en órbita el
44
Puedo extender esta respuesta, señalando qué sucede exactamente en el código de ensamblaje, y que esto realmente viola el estándar, aunque no me llamaría un abogado de idiomas, por lo que no puedo garantizar que no haya un oscuro cláusula que permite explícitamente ese comportamiento. Supongo que el OP estaba más interesado en complicaciones prácticas en compiladores reales, no en compiladores completamente libres de errores y totalmente compatibles (que de hecho no existen, supongo).
chtz
44
Vale la pena mencionar que -ffloat-storeparece ser la forma de prevenir esto.
OrangeDog
116

No será cierto si xes así NaN, ya que las comparaciones NaNson siempre falsas (sí, incluso NaN == NaN). Para todos los demás casos (valores normales, valores subnormales, infinitos, ceros) esta afirmación será verdadera.

El consejo para evitar los ==flotantes se aplica a los cálculos debido a que los números de coma flotante no pueden expresar muchos resultados exactamente cuando se usan en expresiones aritméticas. La asignación no es un cálculo y no hay razón para que la asignación arroje un valor diferente al original.


La evaluación de precisión extendida no debería ser un problema si se sigue el estándar. De <cfloat>heredado de C [5.2.4.2.2.8] ( énfasis mío ):

A excepción de la asignación y la conversión (que eliminan todo el rango y la precisión adicionales) , los valores de las operaciones con operandos flotantes y valores sujetos a las conversiones aritméticas habituales y las constantes flotantes se evalúan en un formato cuyo rango y precisión pueden ser mayores que los requeridos por tipo.

Sin embargo, como los comentarios han señalado, algunos casos con ciertos compiladores, opciones de compilación y objetivos podrían hacer que esto sea paradójicamente falso.

kmdreko
fuente
10
¿Qué pasa si xse calcula en un registro en la primera línea, manteniendo más precisión que el mínimo para a float. El y = xpuede estar en la memoria, manteniendo solo la floatprecisión. Luego, la prueba de igualdad se haría con la memoria contra el registro, con diferentes precisiones y, por lo tanto, sin garantía.
David Schwartz
55
x+pow(b,2)==x+pow(a,3)podría diferir de auto one=x+pow(b,2); auto two=y+pow(a,3); one==twoporque uno podría comparar usando más precisión que el otro (si uno / dos son valores de 64 bits en ram, mientras que los valores intermedistas son 80 bits en fpu). Entonces la asignación puede hacer algo, a veces.
Yakk - Adam Nevraumont
22
@evg ¡Seguro! Mi respuesta simplemente sigue el estándar. Todas las apuestas están desactivadas si le dice a su compilador que no confunda, especialmente cuando habilita las matemáticas rápidas.
kmdreko
11
@Voo Vea la cita en mi respuesta. El valor del RHS se asigna a la variable en el LHS. No hay justificación legal para que el valor resultante del LHS difiera del valor del RHS. Aprecio que varios compiladores tengan errores a este respecto. Pero se supone que si algo está almacenado en un registro no tiene nada que ver con eso.
Carreras de ligereza en órbita el
66
@Voo: en ISO C ++, se supone que el redondeo para escribir ancho ocurre en cualquier asignación. En la mayoría de los compiladores que apuntan a x87, en realidad solo sucede cuando el compilador decide derramar / recargar. Puede forzarlo gcc -ffloat-storepara un cumplimiento estricto. Pero esta pregunta se trata x=y; x==y; sin hacer nada a ninguna de las dos. Si yya está redondeado para caber en un flotador, la conversión a doble o largo doble y viceversa no cambiará el valor. ...
Peter Cordes
34

Sí, yseguramente asumirá el valor de x:

[expr.ass]/2: En la asignación simple (=), el objeto al que hace referencia el operando izquierdo se modifica ([defns.access]) reemplazando su valor con el resultado del operando derecho.

No hay margen de maniobra para asignar otros valores.

(Otros ya han señalado que una comparación de equivalencia ==evaluará los falsevalores de NaN).

El problema habitual con el punto flotante ==es que es fácil no tener el valor que crees que tienes. Aquí, sabemos que los dos valores, sean cuales sean, son los mismos.

Carreras de ligereza en órbita
fuente
77
@ThomasWeller Ese es un error conocido en una implementación consecuentemente no compatible. ¡Aunque es bueno mencionarlo!
Carreras ligeras en órbita el
Al principio, pensé que el lenguaje que aboga por la distinción entre "valor" y "resultado" sería perverso, pero no es necesario que esta distinción sea sin diferencia por el lenguaje de C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 o C5.3, 7.1.6 del borrador de Norma que usted cita.
Eric Towers
@EricTowers Lo siento, ¿puedes aclarar esas referencias? No encuentro lo que estás señalando
carreras de ligereza en órbita el
@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 y C5.3 .
Eric Towers
@EricTowers Sí, todavía no te sigo. Su primer enlace va al índice del Apéndice C (no me dice nada). Sus próximos cuatro enlaces van todos a [expr]. Si voy a ignorar los enlaces y centrarme en las citas, me queda la confusión de que, por ejemplo, C.5.3 no parece abordar el uso del término "valor" o el término "resultado" (aunque sí use "resultado" una vez en su contexto normal en inglés). Quizás podría describir más claramente dónde cree que el estándar hace una distinción y proporcionar una sola cita clara para que esto suceda. ¡Gracias!
Carreras de ligereza en órbita el
3

Sí, en todos los casos (sin tener en cuenta los problemas de NaN y x87), esto será cierto.

Si haces una memcmpprueba en ellos, podrás probar la igualdad mientras comparas NaNs y sNaNs. Esto también requerirá que el compilador tome la dirección de la variable que coaccionará el valor a 32 bits en floatlugar de 80 bits. Esto eliminará los problemas x87. La segunda afirmación aquí pretende no mostrar que ==no comparará los NaN como verdaderos:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Tenga en cuenta que si los NaN tienen una representación interna diferente (es decir, una mantisa diferente), la memcmpcomparación no será verdadera.

SS Anne
fuente
1

En los casos habituales, se evaluaría como verdadero. (o la declaración de afirmación no hará nada)

Editar :

Por "casos habituales" me refiero a que estoy excluyendo los escenarios antes mencionados (como los valores de NaN y las unidades de punto flotante de 80x87) según lo señalado por otros usuarios.

Dada la obsolescencia de los chips 8087 en el contexto actual, el problema está bastante aislado y para que la pregunta sea aplicable en el estado actual de la arquitectura de punto flotante utilizada, es cierto para todos los casos, excepto para NaNs.

(referencia sobre 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Felicitaciones a @chtz por reproducir un buen ejemplo y a @kmdreko por mencionar NaNs: ¡no los conocíamos antes!

Anirban166
fuente
1
Pensé que era completamente posible xestar en un registro de coma flotante mientras yse carga desde la memoria. La memoria puede tener menos precisión que un registro, lo que hace que falle la comparación.
David Schwartz
1
Ese podría ser un caso para un falso, no lo he pensado hasta ahora. (dado que el OP no proporcionó ningún caso especial, supongo que no hay restricciones adicionales)
Anirban166
1
Realmente no entiendo lo que estás diciendo. Según tengo entendido la pregunta, el OP pregunta si copiar un flotador y luego probar la igualdad está garantizado para tener éxito. Su respuesta parece estar diciendo "sí". Estoy preguntando por qué la respuesta no es no.
David Schwartz
66
La edición hace que esta respuesta sea incorrecta. El estándar C ++ requiere que la asignación convierta el valor al tipo de destino; se puede usar un exceso de precisión en las evaluaciones de expresión pero no se puede retener mediante la asignación. Es irrelevante si el valor se mantiene en un registro o memoria; el estándar C ++ requiere que sea, como se escribe el código, un floatvalor sin precisión adicional.
Eric Postpischil
2
@AProgrammer Dado que, en teoría, un compilador con errores (n extremadamente) podría provocar int a=1; int b=a; assert( a==b );una afirmación, creo que solo tiene sentido responder esta pregunta en relación con un compilador que funcione correctamente (aunque posiblemente tenga en cuenta que algunas versiones de algunos compiladores sí / no tienen -se sabe-para hacer esto mal). En términos prácticos, si por alguna razón un compilador no elimina la precisión adicional del resultado de una asignación almacenada en el registro, debe hacerlo antes de usar ese valor.
TripeHound
-1

Sí, devolverá True siempre, excepto si es NaN . Si el valor de la variable es NaN , ¡siempre devuelve False !

Valentin Popescu
fuente