2010-02-10 18 views
7

He encontrado un problema molesto al generar un número de coma flotante. Cuando formato 11.545 con una precisión de 2 decimales en Windows, da salida a "11.55", como era de esperar. Sin embargo, cuando hago lo mismo en Linux, la salida es "11.54".Emular el comportamiento de conversión de cadena de punto flotante de Linux en Windows

Originalmente encontré el problema en Python, pero la investigación posterior mostró que la diferencia está en la biblioteca C en tiempo de ejecución subyacente. (La arquitectura es x86-x64 en ambos casos). La ejecución de la siguiente línea de C produce los diferentes resultados en Windows y Linux, al igual que en Python.

printf("%.2f", 11.545); 

de arrojar más luz sobre este Imprimí el número de 20 cifras decimales ("%.20f"):

Windows: 11.54500000000000000000 
Linux: 11.54499999999999992895 

Sé que 11.545 no se puede almacenar, precisamente, como un número binario. Entonces, lo que parece estar sucediendo es que Linux genera el número que realmente está almacenado con la mejor precisión posible, mientras que Windows emite la representación decimal más simple de él, es decir. trata de adivinar lo que el usuario probablemente haya querido decir.

Mi pregunta es: ¿hay alguna forma (razonable) de emular el comportamiento de Linux en Windows?

(Aunque el comportamiento de Windows es ciertamente el intuitivo, en mi caso, realmente necesito comparar el resultado de un programa de Windows con el de un programa de Linux y el de Windows es el único que puedo cambiar. así, traté de mirar a la fuente de Windows de printf, pero la función real que hace el Float-> conversión de cadenas es _cfltcvt_l y su fuente no parece estar disponible)

EDIT:. la trama se complica ! La teoría sobre este ser causada por una representación imprecisa que podría estar equivocado, porque 0,125 tiene una representación binaria exacta y sigue siendo diferente cuando la salida con '%.2f' % 0.125:

Windows: 0.13 
Linux: 0.12 

Sin embargo, round(0.125, 2) rendimientos 0.13 en Windows y Linux .

+0

Comparar flotantes mediante la conversión a cadenas y la comparación de las cadenas es una idea fundamentalmente no portátil. Además, si está haciendo matemática para llegar a estos valores, normalmente podría terminar obteniendo resultados diferentes (por ejemplo, precisión FPU interna de 32, 64 u 80 bits, diferente precisión de las funciones de la libma), incluso antes de que llegue a formateo –

+0

Una doble tiene aproximadamente 15 dígitos decimales de precisión, al menos para MSVC. Hubiera supuesto que sería similar para una versión de Linux x86, pero tal vez tengan algunas técnicas para proporcionar más (¿es un 'doble' en la versión de Linux de 8 bytes? Es' DBL_DIG' en 'float.h' algo más grande de 15?) Lo que está sucediendo en su compilación de Windows es correcto, ya que con los 15 dígitos de precisión, el valor representa 11.545. Bienvenido al mundo extraño y no siempre tan maravilloso de punto flotante ... –

Respuesta

0

Considere la posibilidad de comparar números de coma flotante con alguna tolerancia/epsilon en su lugar. Esto es mucho más sólido que tratar de hacer coincidir exactamente.

Lo que quiero decir es, excepto decir que dos flotadores son iguales cuando:

f1 == f2 

decir que son iguales cuando:

fabs(f1 - f2) < eps 

Por alguna pequeña eps. Se pueden encontrar más detalles sobre este tema en here.

+0

Sí, normalmente lo haría, pero en este caso estoy comparando cadenas, no flota directamente. Las cadenas son parte de un resultado de programa mucho más grande. Además, si se redondean a 2 lugares, no puedo usar realistamente un eps de 0.02, eso escondería diferencias reales. – EMP

0

Puede intentar restar (o sumar para un número negativo) un pequeño delta que no tendrá ningún efecto en el redondeo para números lo suficientemente lejos de la precisión.

Por ejemplo, si usted está redondeo con %.2f, prueba esta versión en Windows:

printf("%.2f", 11.545 - 0.001); 

números de punto flotante son notoriamente problemático si usted no sabe lo que está pasando debajo de las sábanas. En ese caso, su mejor opción es escribir (o usar) una biblioteca de tipo decimal para aliviar los problemas.


El programa de ejemplo:

#include <stdio.h> 
int main (void) { 
    printf("%.20f\n", 11.545); 
    printf("%.2f\n", 11.545); 
    printf("%.2f\n", 11.545 + 0.001); 
    return 0; 
} 

salidas de ésta en mi entorno Cygwin:

11.54499999999999992895 
11.54 
11.55 

que está bien para su caso específico (que va por el camino equivocado, pero espero que debería aplicarse en en la otra dirección también: debe probarlo) pero debe verificar todo su rango de entrada posible si desea estar seguro de que esto funcionará para todos sus casos.


Actualización:

Evgeny, basado en su comentario:

Funciona para este caso específico, pero no como una solución general. Por ejemplo, si el número que quiero formatear es 0.545 en vez de 11.545, entonces '% .2f'% (0.545 - 0.001) arroja "0.54", mientras que '% .2f'% 0.545 en Linux retorna correctamente '0.55'.

es por eso que dije que tendría que verificar todo el rango para ver si funciona, y por qué afirmé que sería preferible un tipo de datos decimal.

Si desea precisión decimal, eso es lo que tendrá que hacer. Pero es posible que desee considerar los casos en ese rango donde Linux también lo hace (según su comentario) - puede haber una situación en la que Linux y Windows no coinciden con lo que ha encontrado - un tipo decimal probablemente ganó Soluciono eso.

Puede que necesite hacer sus herramientas de comparación un poco más inteligentes en la medida en que pueden ignorar una diferencia de 1 en el último lugar fraccional.

+0

Funciona para este caso específico, pero no como una solución general. Por ejemplo, si el número que quiero formatear es 0.545 en vez de 11.545, entonces ''% .2f'% (0.545 - 0.001)' devuelve "0.54", mientras que ''% .2f'% 0.545' en Linux retorna correctamente" 0.55 " . – EMP

1

El módulo decimal le da acceso a varios modos de redondeo:

import decimal 

fs = ['11.544','11.545','11.546'] 

def convert(f,nd): 
    # we want 'nd' beyond the dec point 
    nd = f.find('.') + nd 
    c1 = decimal.getcontext().copy() 
    c1.rounding = decimal.ROUND_HALF_UP 
    c1.prec = nd 
    d1 = c1.create_decimal(f) 
    c2 = decimal.getcontext().copy() 
    c2.rounding = decimal.ROUND_HALF_DOWN 
    c2.prec = nd 
    d2 = c2.create_decimal(f) 
    print d1, d2 

for f in fs: 
    convert(f,2) 

Usted puede construir un número decimal de un entero o una cadena. En su caso, alimente una cadena con más dígitos de los que desea y trunque estableciendo context.prec.

Aquí hay un enlace a un post pymotw w/una descripción detallada del módulo decimales:

http://broadcast.oreilly.com/2009/08/pymotw-decimal---fixed-and-flo.html

+0

Sí, produce 11.55 en Linux, pero no puedo editar el programa Linux, solo el de Windows. Entonces necesito obtener el Windows para producir 11.54 en su lugar. – EMP

+0

Lo siento por la mala lectura: actualicé la solución con información sobre el módulo decimal. Con suerte, esto le dará el control de redondeo para duplicar el comportamiento de Linux en Windows. – Cyrus

+0

Buena idea, pero, al igual que la respuesta de paxdiablo, no pasa la prueba "0.545", que redondea a "0.54" cuando el programa Linux generará "0.55". – EMP

2

En primer lugar suena como Windows tiene que mal correcto en este caso (no es que esto realmente importa). El estándar C requiere que el valor generado por %.2f sea redondeado al número apropiado de dígitos. El algoritmo mejor conocido para esto es dtoa implementado por David M. Gay.Probablemente pueda transferir esto a Windows o encontrar una implementación nativa.

Si aún no ha leído "Cómo imprimir números de punto flotante con precisión" de Steele and White, busque una copia y léala. Definitivamente es una lectura esclarecedora. Asegúrate de encontrar el original de finales de los 70's. Creo que compré el mío de ACM o IEEE en algún momento.

+0

No, ambas implementaciones son perfectamente válidas. ISO 754 permite varios modos de redondeo; Linux usa round-to-even, y Linux usa round-away-from-0. –

+0

Puede que tengas razón, Glenn - 'round()' no hace esto, pero 'printf()' sí! 'round()' parece ser consistente entre Windows y Linux. – EMP

+1

La función C 'round()' siempre redondea desde cero, así que esperaría que fuera consistente. –

0

Usted puede ser capaz de hacer restar una cantidad pequeña del valor para forzar el redondeo a la baja

print "%.2f"%(11.545-1e-12) 
2

No creo que Windows está haciendo nada en especial inteligente (como tratar de reinterpretar el flotador en la base 10) aquí: supongo que simplemente calcula los primeros 17 dígitos significativos con precisión (lo que daría '11 .545000000000000 ') y luego colocando ceros adicionales en el extremo para completar la cantidad de lugares solicitados después del punto.

Como han dicho otros, los diferentes resultados para 0.125 provienen de Windows usando round-half-up y Linux usando round-half-to-even.

Tenga en cuenta que para Python 3.1 (y Python 2.7, cuando aparece), el resultado de formatear un flotante será independiente de la plataforma (excepto posiblemente en plataformas inusuales).

+0

No creo que sea solo poner ceros adicionales, porque el número no es realmente 11.5450000 ... incluso hasta 17 dígitos. Pero +1 para la información sobre Python 3.1 y 2.7. ¡Esperamos eso! – EMP

+0

La ronda de 17 cifras significativas (¡no 17 lugares después del punto!) Y la fórmula de ceros extra parece funcionar para mí, para este número en particular. Los primeros 17 dígitos parecen 11.54499 ... el 18 es también un 9, así que redondeamos. La suposición fue motivada por la versión original de 1985 de IEEE 754: en la sección 5.6 (Binario <---> conversiones decimales), encontrará el texto "el implementador puede, a su elección, alterar todos los dígitos significativos después del noveno para solo y el decimoséptimo para el doble a otros dígitos decimales, típicamente 0. " Supongo que Microsoft eligió ejercer esta opción. :) –

Cuestiones relacionadas