El siguiente código funciona en Visual Studio 2008 con y sin optimización. Pero solo funciona en g ++ sin optimización (O0).
#include <cstdlib>
#include <iostream>
#include <cmath>
double round(double v, double digit)
{
double pow = std::pow(10.0, digit);
double t = v * pow;
//std::cout << "t:" << t << std::endl;
double r = std::floor(t + 0.5);
//std::cout << "r:" << r << std::endl;
return r / pow;
}
int main(int argc, char *argv[])
{
std::cout << round(4.45, 1) << std::endl;
std::cout << round(4.55, 1) << std::endl;
}
La salida debe ser:
4.5
4.6
Pero g ++ con optimización ( O1
- O3
) dará como resultado:
4.5
4.5
Si agrego la volatile
palabra clave antes de t, funciona, entonces, ¿podría haber algún tipo de error de optimización?
Prueba en g ++ 4.1.2 y 4.4.4.
Aquí está el resultado en ideone: http://ideone.com/Rz937
Y la opción que pruebo en g ++ es simple:
g++ -O2 round.cpp
El resultado más interesante, incluso si activo la /fp:fast
opción en Visual Studio 2008, el resultado sigue siendo correcto.
Otra pregunta:
Me preguntaba, ¿debería activar siempre la -ffloat-store
opción?
Porque la versión g ++ que probé se envía con CentOS / Red Hat Linux 5 y CentOS / Redhat 6 .
Compilé muchos de mis programas en estas plataformas y me preocupa que causen errores inesperados dentro de mis programas. Parece un poco difícil investigar todo mi código C ++ y bibliotecas usadas si tienen tales problemas. ¿Cualquier sugerencia?
¿Alguien está interesado en por qué incluso /fp:fast
encendido, Visual Studio 2008 todavía funciona? ¿Parece que Visual Studio 2008 es más confiable en este problema que g ++?
fuente
Respuestas:
Los procesadores Intel x86 utilizan internamente precisión extendida de 80 bits, mientras
double
que normalmente tiene un ancho de 64 bits. Los diferentes niveles de optimización afectan la frecuencia con la que los valores de punto flotante de la CPU se guardan en la memoria y, por lo tanto, se redondean de precisión de 80 bits a precisión de 64 bits.Utilice la
-ffloat-store
opción gcc para obtener los mismos resultados de punto flotante con diferentes niveles de optimización.Alternativamente, use el
long double
tipo, que normalmente tiene 80 bits de ancho en gcc para evitar el redondeo de precisión de 80 bits a 64 bits.man gcc
lo dice todo:En las compilaciones x86_64, los compiladores usan registros SSE para
float
ydouble
de forma predeterminada, de modo que no se usa precisión extendida y este problema no ocurre.gcc
La opción del compilador-mfpmath
controla eso.fuente
inf
. No existe una buena regla general, las pruebas unitarias pueden darte una respuesta definitiva.Como ya señaló Maxim Yegorushkin en su respuesta, parte del problema es que internamente su computadora está usando una representación de punto flotante de 80 bits. Sin embargo, esto es solo parte del problema. La base del problema es que cualquier número de la forma n.nn5 no tiene una representación flotante binaria exacta. Esos casos de esquina son siempre números inexactos.
Si realmente desea que su redondeo pueda redondear de manera confiable estos casos de esquina, necesita un algoritmo de redondeo que aborde el hecho de que n.n5, n.nn5 o n.nnn5, etc. (pero no n.5) siempre es inexacto. Encuentre el caso de la esquina que determina si algún valor de entrada se redondea hacia arriba o hacia abajo y devuelva el valor redondeado hacia arriba o hacia abajo según una comparación con este caso de esquina. Y debe tener cuidado de que un compilador de optimización no coloque ese caso de esquina encontrado en un registro de precisión extendido.
Consulte ¿Cómo Excel redondea correctamente los números flotantes aunque sean imprecisos?para tal algoritmo.
O simplemente puede vivir con el hecho de que las cajas de las esquinas a veces se redondean erróneamente.
fuente
Los diferentes compiladores tienen diferentes configuraciones de optimización. Algunas de esas configuraciones de optimización más rápidas no mantienen reglas estrictas de punto flotante de acuerdo con IEEE 754 . Visual Studio tiene una configuración específica,
/fp:strict
,/fp:precise
,/fp:fast
,, donde/fp:fast
viola la norma en lo que se puede hacer. Puede encontrar que este indicador es lo que controla la optimización en tales configuraciones. También puede encontrar una configuración similar en GCC que cambia el comportamiento.Si este es el caso, lo único diferente entre los compiladores es que GCC buscaría el comportamiento de punto flotante más rápido de forma predeterminada en optimizaciones más altas, mientras que Visual Studio no cambia el comportamiento de punto flotante con niveles de optimización más altos. Por lo tanto, puede que no sea necesariamente un error real, sino el comportamiento previsto de una opción que no sabía que estaba activando.
fuente
-ffast-math
interruptor para GCC que, y no está activado por ninguno de los-O
niveles de optimización desde la cita: "puede dar como resultado una salida incorrecta para programas que dependen de una implementación exacta de las reglas / especificaciones IEEE o ISO para funciones matemáticas".-ffast-math
y algunas otras cosas en mig++ 4.4.3
y todavía no puedo reproducir el problema.-ffast-math
obtengo4.5
en ambos casos para niveles de optimización superiores a0
.4.5
con-O1
y-O2
, pero no con-O0
y-O3
en GCC 4.4.3, pero con-O1,2,3
en GCC 4.6.1.)Esto implica que el problema está relacionado con las declaraciones de depuración. Y parece que hay un error de redondeo causado al cargar los valores en los registros durante las declaraciones de salida, razón por la cual otros descubrieron que puede solucionarlo con
-ffloat-store
Para ser impertinente, tiene que haber una razón por la que algunos programadores no se encienden
-ffloat-store
, de lo contrario no existiría la opción (del mismo modo, debe haber una razón por la que algunos programadores no se encienden-ffloat-store
). No recomendaría encenderlo o apagarlo siempre. Activarlo evita algunas optimizaciones, pero desactivarlo permite el tipo de comportamiento que está obteniendo.Pero, en general, existe cierta discrepancia entre los números de coma flotante binarios (como los que usa la computadora) y los números de coma flotante decimal (con los que la gente está familiarizada), y esa discrepancia puede causar un comportamiento similar al que obtiene (para ser claros, el comportamiento que obtiene no es causado por esta falta de coincidencia, sino similar puede ser comportamiento ). La cuestión es que, dado que ya tiene cierta vaguedad al tratar con el punto flotante, no puedo decir que
-ffloat-store
eso lo mejore o empeore.En su lugar, es posible que desee buscar otras soluciones al problema que está tratando de resolver (desafortunadamente, Koenig no apunta al documento real, y realmente no puedo encontrar un lugar "canónico" obvio para él, así que Tendré que enviarte a Google ).
Si no está redondeando para fines de salida, probablemente miraría
std::modf()
(incmath
) ystd::numeric_limits<double>::epsilon()
(inlimits
). Pensando en laround()
función original , creo que sería más limpio reemplazar la llamada astd::floor(d + .5)
con una llamada a esta función:Creo que eso sugiere la siguiente mejora:
Una nota simple:
std::numeric_limits<T>::epsilon()
se define como "el número más pequeño sumado a 1 que crea un número distinto de 1." Por lo general, es necesario utilizar un épsilon relativo (es decir, escalar épsilon de alguna manera para tener en cuenta el hecho de que está trabajando con números distintos de "1"). La suma ded
,.5
ystd::numeric_limits<double>::epsilon()
debe estar cerca de 1, por lo que agrupar esa suma significa questd::numeric_limits<double>::epsilon()
será sobre el tamaño adecuado para lo que estamos haciendo. En todo caso,std::numeric_limits<double>::epsilon()
será demasiado grande (cuando la suma de los tres es menor que uno) y puede hacer que redondeemos algunos números cuando no deberíamos.Hoy en día, deberías considerarlo
std::nearbyint()
.fuente
x - nextafter(x, INFINITY)
está relacionado con 1 ulp para x (pero no lo use; estoy seguro de que hay casos de esquina y lo acabo de inventar). El ejemplo de referencia de cpp paraepsilon()
tiene un ejemplo de escalado para obtener un error relativo basado en ULP .-ffloat-store
es: no use x87 en primer lugar. Utilice matemáticas SSE2 (binarios de 64 bits, o-mfpmath=sse -msse2
para hacer binarios viejos y crujientes de 32 bits), porque SSE / SSE2 tiene provisionales sin precisión adicional.double
y lasfloat
variables en los registros XMM están realmente en formato IEEE de 64 o 32 bits. (A diferencia de x87, donde los registros son siempre de 80 bits y el almacenamiento en memoria se redondea a 32 o 64 bits)La respuesta aceptada es correcta si está compilando en un destino x86 que no incluye SSE2. Todos los procesadores x86 modernos admiten SSE2, por lo que, si puede aprovecharlo, debería:
Analicemos esto.
-mfpmath=sse -msse2
. Esto realiza el redondeo mediante el uso de registros SSE2, que es mucho más rápido que almacenar cada resultado intermedio en la memoria. Tenga en cuenta que este ya es el predeterminado en GCC para x86-64. De la wiki de GCC :-ffp-contract=off
. Sin embargo, controlar el redondeo no es suficiente para una coincidencia exacta. Las instrucciones FMA (fusionada multiplicar-añadir) pueden cambiar el comportamiento de redondeo en comparación con sus contrapartes no fusionadas, por lo que debemos deshabilitarlo. Este es el predeterminado en Clang, no en GCC. Como se explica en esta respuesta :Al deshabilitar FMA, obtenemos resultados que coinciden exactamente en la depuración y la liberación, a costa de algo de rendimiento (y precisión). Todavía podemos aprovechar otros beneficios de rendimiento de SSE y AVX.
fuente
Profundicé más en este problema y puedo aportar más precisiones. Primero, las representaciones exactas de 4.45 y 4.55 según gcc en x84_64 son las siguientes (con libquadmath para imprimir la última precisión):
Como dijo Maxim anteriormente, el problema se debe al tamaño de 80 bits de los registros FPU.
Pero, ¿por qué el problema nunca ocurre en Windows? en IA-32, el x87 FPU se configuró para usar una precisión interna para la mantisa de 53 bits (equivalente a un tamaño total de 64 bits :)
double
. Para Linux y Mac OS, se utilizó la precisión predeterminada de 64 bits (equivalente a un tamaño total de 80 bits :)long double
. Entonces, el problema debería ser posible, o no, en estas diferentes plataformas cambiando la palabra de control de la FPU (asumiendo que la secuencia de instrucciones desencadenaría el error). El problema se informó a gcc como error 323 (¡lea al menos el comentario 92!).Para mostrar la precisión de la mantisa en Windows, puede compilar esto en 32 bits con VC ++:
y en Linux / Cygwin:
Tenga en cuenta que con gcc puede establecer la precisión de FPU con
-mpc32/64/80
, aunque se ignora en Cygwin. Pero ten en cuenta que modificará el tamaño de la mantisa, pero no del exponente, dejando la puerta abierta a otros tipos de comportamientos diferentes.En la arquitectura x86_64, SSE se usa como lo dice tmandry , por lo que el problema no ocurrirá a menos que fuerce el antiguo x87 FPU para computación FP con
-mfpmath=387
, oa menos que compile en modo de 32 bits con-m32
(necesitará un paquete multilib). Podría reproducir el problema en Linux con diferentes combinaciones de banderas y versiones de gcc:Probé algunas combinaciones en Windows o Cygwin con VC ++ / gcc / tcc pero el error nunca apareció. Supongo que la secuencia de instrucción generada no es la misma.
Finalmente, tenga en cuenta que una forma exótica de prevenir este problema con 4.45 o 4.55 sería usar
_Decimal32/64/128
, pero el soporte es realmente escaso ... ¡Pasé mucho tiempo solo para poder hacer un printflibdfp
!fuente
Personalmente, he tenido el mismo problema yendo en sentido contrario, de gcc a VS. En la mayoría de los casos, creo que es mejor evitar la optimización. La única vez que vale la pena es cuando se trata de métodos numéricos que involucran grandes conjuntos de datos de punto flotante. Incluso después de desmontar, a menudo me decepcionan las opciones de los compiladores. Muy a menudo, es más fácil usar elementos intrínsecos del compilador o simplemente escribir el ensamblado usted mismo.
fuente