¿Resultado de coma flotante diferente con la optimización habilitada? ¿Error del comstackdor?

El siguiente código funciona en Visual Studio 2008 con y sin optimización. Pero solo funciona en g ++ sin optimización (O0).

#include  #include  #include  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; } 

El resultado debería ser:

 4.5 4.6 

Pero g ++ con optimización ( O1O3 ) dará como resultado:

 4.5 4.5 

Si agrego la palabra clave volatile antes de t, funciona, ¿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 /fp:fast opción /fp:fast en Visual Studio 2008, el resultado sigue siendo correcto.

Otra pregunta:

Me preguntaba, ¿debería activar siempre la opción -ffloat-store ?

Porque la versión de g ++ que probé se envía con CentOS / Red Hat Linux 5 y CentOS / Redhat 6 .

Recopilé muchos de mis progtwigs bajo estas plataformas, y me preocupa que cause errores inesperados dentro de mis progtwigs. Parece un poco difícil investigar todo mi código C ++ y las bibliotecas usadas si tienen tales problemas. ¿Cualquier sugerencia?

¿Alguien está interesado en por qué even /fp:fast activó, Visual Studio 2008 aún funciona? Parece que Visual Studio 2008 es más confiable en este problema que g ++?

Los procesadores Intel x86 usan internamente una precisión extendida de 80 bits, mientras que el double normalmente tiene una anchura de 64 bits. 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.

Use la -ffloat-store gcc para obtener los mismos resultados de punto flotante con diferentes niveles de optimización.

Alternativamente, utilice el tipo long double , que normalmente tiene 80 bits de ancho en gcc para evitar el redondeo de 80 bits a 64 bits de precisión.

man gcc dice todo:

  -ffloat-store Do not store floating point variables in registers, and inhibit other options that might change whether a floating point value is taken from a register or memory. This option prevents undesirable excess precision on machines such as the 68000 where the floating registers (of the 68881) keep more precision than a "double" is supposed to have. Similarly for the x86 architecture. For most programs, the excess precision does only good, but a few programs rely on the precise definition of IEEE floating point. Use -ffloat-store for such programs, after modifying them to store all pertinent intermediate computations into variables. 

La salida debería ser: 4.5 4.6 Esa sería la salida si tuvieras una precisión infinita, o si estuvieras trabajando con un dispositivo que utilizara una representación de punto flotante basada en el decimal en lugar del binario. Pero, no lo eres. La mayoría de las computadoras usan el estándar de coma flotante IEEE binario.

Como ya mencionó Maxim Yegorushkin en su respuesta, parte del problema es que internamente su computadora está usando una representación de coma flotante de 80 bits. Esto es solo parte del problema, sin embargo. 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 sea capaz de redondear de manera confiable estos casos de esquina, necesita un algoritmo de redondeo que resuelva el hecho de que n.n5, n.nn5 o n.nnn5, etc. (pero no n.5) es siempre inexacto. Encuentre el caso de esquina que determina si algún valor de entrada redondea hacia arriba o hacia abajo y devuelve el valor redondeado hacia arriba o redondeado en base a una comparación con este caso de esquina. Y debe tener cuidado de que un comstackdor de optimización no coloque la caja de esquina encontrada en un registro de precisión ampliado.

Consulte ¿Cómo redondea Excel con éxito los números flotantes aunque sean imprecisos? para tal algoritmo.

O simplemente puede vivir con el hecho de que los casos de las esquinas a veces se redondean erróneamente.

Los diferentes comstackdores 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 infringe el estándar sobre lo que se puede hacer. Puede encontrar que este indicador es lo que controla la optimización en dichos ajustes. También puede encontrar una configuración similar en GCC que cambia el comportamiento.

Si este es el caso, entonces, lo único que es diferente entre los comstackdores es que GCC buscará 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, podría no ser necesariamente un error real, sino el comportamiento previsto de una opción que no sabía que estaba activando.

Para aquellos que no pueden reproducir el error: no eliminen los comentarios de las reglas de depuración, afectan el resultado.

Esto implica que el problema está relacionado con las instrucciones de depuración. Y parece que hay un error de redondeo causado al cargar los valores en los registros durante las declaraciones de salida, por lo que otros descubrieron que puede solucionarlo con -ffloat-store

Otra pregunta:

Me preguntaba, ¿debería encender siempre la opción de la -ffloat-store ?

Para ser frívolo, debe haber una razón por la que algunos progtwigdores no encienden -ffloat-store , de lo contrario la opción no existiría (asimismo, debe haber una razón por la que algunos progtwigdores activan -ffloat-store ). No recomendaría encenderlo o apagarlo siempre. Activarlo evita algunas optimizaciones, pero apagarlo permite el tipo de comportamiento que está recibiendo.

Pero, en general, existe una discrepancia entre los números de coma flotante binarios (como los usa la computadora) y los números de coma flotante decimal (con los que la gente está familiarizada), y esa falta de coincidencia puede causar un comportamiento similar a lo que obtienes (para ser claro, el comportamiento lo que estás obteniendo no es causado por esta falta de coincidencia, pero un comportamiento similar puede ser). La cuestión es que, dado que ya tiene cierta imprecisión al tratar con el punto flotante, no puedo decir que -ffloat-store lo mejora o empeora.

En su lugar, es posible que desee buscar otras soluciones para el problema que está tratando de resolver (lamentablemente, Koenig no señala el documento real, y realmente no puedo encontrar un lugar “canónico” obvio para él, entonces Te enviaré a Google ).


Si no está redondeando para fines de salida, probablemente miraría std::modf() (en cmath ) y std::numeric_limits::epsilon() (en limits ). Pensando en la función round() original, creo que sería más limpio reemplazar la llamada a std::floor(d + .5) con una llamada a esta función:

 // this still has the same problems as the original rounding function int round_up(double d) { // return value will be coerced to int, and truncated as expected // you can then assign the int to a double, if desired return d + 0.5; } 

Creo que sugiere la siguiente mejora:

 // this won't work for negative d ... // this may still round some numbers up when they should be rounded down int round_up(double d) { double floor; d = std::modf(d, &floor); return floor + (d + .5 + std::numeric_limits::epsilon()); } 

Una nota simple: std::numeric_limits::epsilon() se define como “el número más pequeño agregado a 1 que crea un número no igual a 1”. Por lo general, necesita utilizar un épsilon relativo (es decir, epsilon de escala de alguna manera para tener en cuenta el hecho de que está trabajando con números que no sean “1”). La sum de d , .5 y std::numeric_limits::epsilon() debe estar cerca de 1, por lo que agrupar esa sum significa que std::numeric_limits::epsilon() tendrá aproximadamente el tamaño correcto para lo que estamos haciendo En todo caso, std::numeric_limits::epsilon() será demasiado grande (cuando la sum de los tres es menor que uno) y puede hacer que redondeemos algunos números cuando no deberíamos.


Hoy en día, debes considerar std::nearbyint() .

La respuesta aceptada es correcta si está comstackndo para un objective x86 que no incluye SSE2. Todos los procesadores x86 modernos admiten SSE2, por lo que si puede aprovecharlo, debe:

 -mfpmath=sse -msse2 -ffp-contract=off 

Vamos a romper esto.

-mfpmath=sse -msse2 . Esto realiza el redondeo mediante el uso de registros SSE2, que es mucho más rápido que almacenar todos los resultados intermedios en la memoria. Tenga en cuenta que esto ya es el predeterminado en GCC para x86-64. Desde la wiki de GCC :

En los procesadores x86 más modernos que admiten SSE2, al especificar las opciones del comstackdor -mfpmath=sse -msse2 garantiza que todas las operaciones de flotación y doble se realicen en registros SSE y se -mfpmath=sse -msse2 correctamente. Estas opciones no afectan el ABI y, por lo tanto, deben usarse siempre que sea posible para obtener resultados numéricos predecibles.

-ffp-contract=off . Sin embargo, controlar el redondeo no es suficiente para una coincidencia exacta. Las instrucciones FMA (fusted multiple-add) pueden cambiar el comportamiento de redondeo frente a sus contrapartidas no fusionadas, por lo que debemos deshabilitarlo. Este es el valor predeterminado en Clang, no en GCC. Como se explica en esta respuesta :

Una FMA tiene solo un redondeo (efectivamente mantiene una precisión infinita para el resultado de multiplicación temporal interna), mientras que un ADD + MUL tiene dos.

Al deshabilitar FMA, obtenemos resultados que coinciden exactamente con la depuración y la versión, a costa de algún rendimiento (y precisión). Todavía podemos aprovechar otros beneficios de rendimiento de SSE y AVX.

Personalmente, he atacado el mismo problema yendo para otro lado, desde gcc hasta 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 matrices de datos de punto flotante. Incluso después de desmontar, a menudo me siento decepcionado por las elecciones de los comstackdores. Muy a menudo es más fácil usar los intrínsecos del comstackdor o simplemente escribir el ensamble usted mismo.

Profundicé en este problema y puedo aportar más precisiones. Primero, las representaciones exactas de 4.45 y 4.55 de acuerdo con gcc en x84_64 son las siguientes (con libquadmath para imprimir la última precisión):

 float 32: 4.44999980926513671875 double 64: 4.45000000000000017763568394002504646778106689453125 doublex 80: 4.449999999999999999826527652402319290558807551860809326171875 quad 128: 4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125 float 32: 4.55000019073486328125 double 64: 4.54999999999999982236431605997495353221893310546875 doublex 80: 4.550000000000000000173472347597680709441192448139190673828125 quad 128: 4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875 

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, la FPU x87 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 plataformas diferentes cambiando la palabra de control de la FPU (suponiendo que la secuencia de instrucciones activaría el error). El problema se informó a gcc como error 323 (¡lea al menos el comentario 92!).

Para mostrar la precisión de mantisa en Windows, puede comstackr esto en 32 bits con VC ++:

 #include "stdafx.h" #include  #include  int main(void) { char t[] = { 64, 53, 24, -1 }; unsigned int cw = _control87(0, 0); printf("mantissa is %d bits\n", t[(cw >> 16) & 3]); } 

y en Linux / Cygwin:

 #include  int main(int argc, char **argv) { char t[] = { 24, -1, 53, 64 }; unsigned int cw = 0; __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw)); printf("mantissa is %d bits\n", t[(cw >> 8) & 3]); } 

Tenga en cuenta que con gcc puede establecer la precisión de la FPU con -mpc32/64/80 , aunque se ignora en Cygwin. Pero tenga en cuenta que modificará el tamaño de la mantisa, pero no el exponente, dejando que la puerta se abra a otro tipo de comportamiento diferente.

En la architecture x86_64, SSE se usa como dice tmandry , por lo que el problema no ocurrirá a menos que fuerce la antigua FPU x87 para computación FP con -mfpmath=387 , o a menos que compile en modo de 32 bits con -m32 (necesitará multilibrar paquete). Podría reproducir el problema en Linux con diferentes combinaciones de banderas y versiones de gcc:

 g++-5 -m32 floating.cpp -O1 g++-8 -mfpmath=387 floating.cpp -O1 

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 evitar 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 una libdfp con libdfp !