c++ - normalizada - Igualdad de punto flotante
punto flotante ejemplos (6)
Sin embargo, me pregunto, ¿hay algún caso, cuando se usa == está perfectamente bien?
Claro que hay Una categoría de ejemplos son los usos que no implican cómputo, por ejemplo, establecedores que solo deben ejecutar cambios:
void setRange(float min, float max)
{
if(min == m_fMin && max == m_fMax)
return;
m_fMin = min;
m_fMax = max;
// Do something with min and/or max
emit rangeChanged(min, max);
}
Ver también ¿Es flotante == alguna vez está bien? y ¿Está el punto flotante == alguna vez OK? .
Es de conocimiento común que hay que tener cuidado al comparar los valores de coma flotante. Usualmente, en lugar de usar ==
, usamos algunas pruebas de igualdad basadas en epsilon o ULP.
Sin embargo, me pregunto, ¿hay algún caso, cuando se usa ==
está perfectamente bien?
Mire este fragmento simple, ¿qué casos tienen garantía de éxito?
void fn(float a, float b) {
float l1 = a/b;
float l2 = a/b;
if (l1==l1) { } // case a)
if (l1==l2) { } // case b)
if (l1==a/b) { } // case c)
if (l1==5.0f/3.0f) { } // case d)
}
int main() {
fn(5.0f, 3.0f);
}
Nota: he verificado this y this , pero no cubren (todos) mis casos.
Nota 2: parece que tengo que agregar algo más de información, por lo que las respuestas pueden ser útiles en la práctica: me gustaría saber:
- lo que dice el estándar de C ++
- ¿Qué sucede si una implementación en C ++ sigue a IEEE-754?
Esta es la única declaración relevante que encontré en el borrador del estándar actual :
La representación del valor de los tipos de coma flotante está definida por la implementación. [Nota: este documento no impone requisitos sobre la precisión de las operaciones de coma flotante; ver también [support.limits]. - nota final]
Entonces, ¿significa esto que incluso el "caso a)" se define la implementación? Quiero decir, l1==l1
es definitivamente una operación de coma flotante. Entonces, si una implementación es "inexacta", ¿podría l1==l1
ser falso?
Creo que esta pregunta no es un duplicado de this . Esa pregunta no aborda ninguno de los casos que estoy preguntando. El mismo tema, diferente pregunta. Me gustaría tener respuestas específicamente para el caso a) -d), para las cuales no puedo encontrar respuestas en la pregunta duplicada.
Asumiendo la semántica de IEEE 754, definitivamente hay algunos casos donde puedes hacer esto. Los cálculos de números de coma flotante convencionales son exactos siempre que pueden, que por ejemplo incluyen (pero no se limitan a) todas las operaciones básicas donde los operandos y los resultados son enteros.
Entonces, si sabes a ciencia cierta que no haces nada que resulte en algo irrepresentable, estás bien. Por ejemplo
float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed
La situación solo empeora si tiene cálculos con resultados que no son exactamente representables (o que involucran operaciones que no son exactas) y cambia el orden de las operaciones.
Tenga en cuenta que el estándar de C ++ en sí mismo no garantiza la semántica de IEEE 754, pero eso es lo que puede esperar tratar la mayor parte del tiempo.
El caso (a) falla si a == b == 0.0
. En este caso, la operación produce NaN, y por definición (IEEE, no C) NaN ≠ NaN.
Los casos (b) y (c) pueden fallar en el cálculo paralelo cuando los modos de coma flotante (u otros modos de cálculo) se cambian en el medio de la ejecución de este subproceso. Visto este en la práctica, desafortunadamente.
El caso (d) puede ser diferente porque el compilador (en alguna máquina) puede elegir doblar constantemente el cálculo de 5.0f/3.0f
y reemplazarlo con el resultado constante (de precisión no especificada), mientras que a/b
debe calcularse en tiempo de ejecución en la máquina de destino (que puede ser radicalmente diferente). De hecho, los cálculos intermedios se pueden realizar con precisión arbitraria. He visto diferencias en arquitecturas Intel antiguas cuando el cálculo intermedio se realizaba en coma flotante de 80 bits, un formato que el lenguaje ni siquiera admitía directamente.
En mi humilde opinión, no debe confiar en el operador ==
porque tiene muchos casos de esquina. El mayor problema es el redondeo y la precisión extendida. En el caso de x86, las operaciones de coma flotante se pueden realizar con mayor precisión de la que puede almacenar en variables (si utiliza coprocesadores, SSE operaciones SSE IIRC utilizan la misma precisión que el almacenamiento).
Esto generalmente es bueno, pero esto causa problemas como: 1./2 != 1./2
porque un valor es variable de forma y el segundo es de registro de coma flotante. En los casos más simples, funcionará, pero si agrega otras operaciones de coma flotante, el compilador podría decidir dividir algunas variables en la pila, cambiando sus valores, cambiando así el resultado de la comparación.
Para tener una certeza del 100%, necesita ver el ensamblaje y ver qué operaciones se realizaron antes en ambos valores. Incluso la orden puede cambiar el resultado en casos no triviales.
En general, ¿qué sentido tiene usar ==
? Debe usar algoritmos que sean estables. Esto significa que funcionan incluso si los valores no son iguales, pero aún dan los mismos resultados. El único lugar donde sé que ==
podría ser útil es serializar / deserializar donde sabes exactamente qué resultado quieres y puedes modificar la serialización para archivar tu objetivo.
Los casos de riesgo pueden "funcionar". Los casos prácticos aún pueden fallar. Un problema adicional es que a menudo la optimización provocará pequeñas variaciones en la forma en que se realiza el cálculo de modo que simbólicamente los resultados sean iguales pero numéricamente sean diferentes. El ejemplo anterior podría, teóricamente, fallar en tal caso. Algunos compiladores ofrecen una opción para producir resultados más consistentes a un costo de rendimiento. Aconsejaría "siempre" evitar la igualdad de los números de coma flotante.
La igualdad de las mediciones físicas, así como los flotadores almacenados digitalmente, a menudo no tiene sentido. Entonces, si comparas si las carrozas son iguales en tu código, probablemente estés haciendo algo mal. Por lo general, desea mayor o menor que o dentro de una tolerancia. Con frecuencia, el código puede reescribirse para evitar estos tipos de problemas.
Solo a) yb) tienen garantizado el éxito en cualquier implementación correcta (consulte la jerga legal a continuación para obtener más detalles), ya que comparan dos valores que se han derivado de la misma manera y redondeados a precisión float
. En consecuencia, se garantiza que ambos valores comparados sean idénticos al último bit.
Los casos c) yd) pueden fallar porque el cálculo y la comparación posterior pueden llevarse a cabo con una precisión mayor que el float
. El redondeo diferente del double
debería ser suficiente para suspender la prueba.
Sin embargo, tenga en cuenta que los casos a) yb) pueden fallar si se trata de infinitos o NAN.
Legalese
Utilizando el borrador de trabajo del estándar N3242 C ++ 11, encuentro lo siguiente:
En el texto que describe la expresión de asignación, se establece explícitamente que tiene lugar la conversión de tipo, [expr.ass] 3:
Si el operando de la izquierda no es del tipo de clase, la expresión se convierte implícitamente (Cláusula 4) en el tipo cv no calificado del operando de la izquierda.
La cláusula 4 se refiere a las conversiones estándar [conv], que contienen lo siguiente en las conversiones de coma flotante, [conv.double] 1:
Un prvalue de tipo de coma flotante se puede convertir a un prvalue de otro tipo de coma flotante. Si el valor fuente puede representarse exactamente en el tipo de destino, el resultado de la conversión es esa representación exacta. Si el valor fuente está entre dos valores de destino adyacentes, el resultado de la conversión es una elección definida por la implementación de cualquiera de esos valores. De lo contrario, el comportamiento no está definido.
(Énfasis mío)
Por lo tanto, tenemos la garantía de que el resultado de la conversión está realmente definido, a menos que tratemos con valores fuera del rango representable (como float a = 1e300
, que es UB).
Cuando la gente piensa acerca de "la representación interna del punto flotante puede ser más precisa que la visible en el código", piensan en la siguiente oración del estándar, [expr] 11:
Los valores de los operandos flotantes y los resultados de las expresiones flotantes se pueden representar con mayor precisión y rango que el requerido por el tipo; los tipos no son cambiados por eso.
Tenga en cuenta que esto se aplica a operandos y resultados , no a variables. Esto se destaca en la nota de pie de página adjunta 60:
Los operadores de reparto y asignación aún deben realizar sus conversiones específicas como se describe en 5.4, 5.2.9 y 5.17.
(Supongo que esta es la nota a pie de página que Maciej Piechotka quiso decir en los comentarios: la numeración parece haber cambiado en la versión del estándar que ha estado usando).
Entonces, cuando digo float a = some_double_expression;
, Tengo la garantía de que el resultado de la expresión en realidad se redondea para ser representable por un float
(invocando UB solo si el valor está fuera de límites), y a
testamento se referirá a ese valor redondeado después.
Una implementación podría especificar que el resultado del redondeo es aleatorio y así romper los casos a) yb). Sin embargo, una implementación sensata no hará eso.