float - round c# decimal
Error de dos vueltas en el método Double.ToString de.NET (4)
La pregunta: ¿no es esto un error?
Sí. Ver este PR en GitHub . La razón de redondear dos veces AFAK es para el resultado de formato "bonito" pero introduce un error como ya ha descubierto aquí. Intentamos solucionarlo: eliminamos la conversión de precisión de 15 dígitos, pasamos directamente a la conversión de precisión de 17 dígitos. La mala noticia es que es un cambio radical y romperá mucho las cosas. Por ejemplo, uno de los casos de prueba se romperá:
10:12:26 Assert.Equal() Failure 10:12:26 Expected: 1.1 10:12:26 Actual: 1.1000000000000001
La solución afectaría a un gran conjunto de bibliotecas existentes, por lo que finalmente este PR se ha cerrado por ahora. Sin embargo, el equipo .NET Core todavía está buscando la posibilidad de solucionar este error. Bienvenido a unirse a la discusión.
Matemáticamente, considere para esta pregunta el número racional
8725724278030350 / 2**48
donde **
en el denominador denota exponenciación, es decir, el denominador es 2
a la 48.ª potencia. (La fracción no está en los términos más bajos, reducible en 2.) Este número es exactamente representable como un sistema. System.Double
. Su expansión decimal es
31.0000000000000''49''73799150320701301097869873046875 (exact)
donde los apóstrofes no representan los dígitos que faltan, sino que simplemente marcan los bordes donde redondean a 15 resp. Se deben realizar 17 dígitos.
Tenga en cuenta lo siguiente: si este número se redondea a 15 dígitos, el resultado será 31
(seguido de trece 0
s) porque los siguientes dígitos ( 49...
) comienzan con un 4
(es decir, redondeo hacia abajo ). Pero si el número primero se redondea a 17 dígitos y luego se redondea a 15 dígitos, el resultado podría ser 31.0000000000001
. Esto se debe a que redondea el primer redondeo aumentando los 49...
dígitos a 50 (terminates)
(los siguientes dígitos fueron 73...
), y el segundo redondeo podría redondearse nuevamente (cuando la regla de redondeo del punto medio dice "redonda lejos de cero ").
(Hay muchos más números con las características anteriores, por supuesto).
Ahora, resulta que la representación de cadena estándar de .NET de este número es "31.0000000000001"
. La pregunta: ¿no es esto un error? Por representación de cadena estándar nos referimos a la String
producida por el método de instancia de los parámetros Double.ToString()
que, por supuesto, es idéntico a lo que produce ToString("G")
.
Una cosa interesante a tener en cuenta es que si lanzas el número anterior a System.Decimal
entonces obtienes un decimal
que es 31
exactamente! Consulte esta pregunta sobre el desbordamiento de la pila para ver un análisis del hecho sorprendente de que el lanzamiento de un Double
a Decimal
implica el primer redondeo a 15 dígitos. Esto significa que la conversión a Decimal
hace una ronda correcta a 15 dígitos, mientras que llamar a ToSting()
hace una incorrecta.
En resumen, tenemos un número de punto flotante que, cuando se 31.0000000000001
al usuario, es 31.0000000000001
, pero cuando se convierte a Decimal
(donde 29 dígitos están disponibles), se convierte en 31
exactamente. Esto es desafortunado.
Aquí hay un código de C # para que puedas verificar el problema:
static void Main()
{
const double evil = 31.0000000000000497;
string exactString = DoubleConverter.ToExactString(evil); // Jon Skeet, http://csharpindepth.com/Articles/General/FloatingPoint.aspx
Console.WriteLine("Exact value (Jon Skeet): {0}", exactString); // writes 31.00000000000004973799150320701301097869873046875
Console.WriteLine("General format (G): {0}", evil); // writes 31.0000000000001
Console.WriteLine("Round-trip format (R): {0:R}", evil); // writes 31.00000000000005
Console.WriteLine();
Console.WriteLine("Binary repr.: {0}", String.Join(", ", BitConverter.GetBytes(evil).Select(b => "0x" + b.ToString("X2"))));
Console.WriteLine();
decimal converted = (decimal)evil;
Console.WriteLine("Decimal version: {0}", converted); // writes 31
decimal preciseDecimal = decimal.Parse(exactString, CultureInfo.InvariantCulture);
Console.WriteLine("Better decimal: {0}", preciseDecimal); // writes 31.000000000000049737991503207
}
El código anterior usa el método ToExactString
de Skeet. Si no desea usar sus cosas (se puede encontrar a través de la URL), simplemente elimine las líneas de código anteriores que dependen de exactString
. Todavía se puede ver cómo el Double
en cuestión ( evil
) se redondea y se lanza.
ADICIÓN:
OK, entonces probé algunos números más, y aquí hay una tabla:
exact value (truncated) "R" format "G" format decimal cast
------------------------- ------------------ ---------------- ------------
6.00000000000000''53''29... 6.0000000000000053 6.00000000000001 6
9.00000000000000''53''29... 9.0000000000000053 9.00000000000001 9
30.0000000000000''49''73... 30.00000000000005 30.0000000000001 30
50.0000000000000''49''73... 50.00000000000005 50.0000000000001 50
200.000000000000''51''15... 200.00000000000051 200.000000000001 200
500.000000000000''51''15... 500.00000000000051 500.000000000001 500
1020.00000000000''50''02... 1020.000000000005 1020.00000000001 1020
2000.00000000000''50''02... 2000.000000000005 2000.00000000001 2000
3000.00000000000''50''02... 3000.000000000005 3000.00000000001 3000
9000.00000000000''54''56... 9000.0000000000055 9000.00000000001 9000
20000.0000000000''50''93... 20000.000000000051 20000.0000000001 20000
50000.0000000000''50''93... 50000.000000000051 50000.0000000001 50000
500000.000000000''52''38... 500000.00000000052 500000.000000001 500000
1020000.00000000''50''05... 1020000.000000005 1020000.00000001 1020000
La primera columna proporciona el valor exacto (aunque truncado) que representa el Double
. La segunda columna da la representación de cadena de la cadena de formato "R"
. La tercera columna da la representación de cadena habitual. Y finalmente la cuarta columna da el System.Decimal
que resulta de convertir este Double
.
Concluimos lo siguiente:
- Redondea a 15 dígitos por
ToString()
y redondea a 15 dígitos por conversión aDecimal
en desacuerdo en muchos casos - La conversión a
Decimal
también se redondea incorrectamente en muchos casos, y los errores en estos casos no se pueden describir como errores "dos veces al máximo" - En mi caso,
ToString()
parece producir un número mayor que la conversiónDecimal
cuando no están de acuerdo (no importa cuál de las dos rondas es correcta)
Solo experimenté con casos como el anterior. No he comprobado si hay errores de redondeo con números de otras "formas".
Entonces, a partir de sus experimentos, parece que Double.ToString
no realiza el redondeo correcto.
Eso es bastante desafortunado, pero no particularmente sorprendente: hacer el redondeo correcto para conversiones binarias a decimales no es trivial, y también es potencialmente bastante lento, requiriendo aritmética de multiprecision en casos de esquina. Consulte el código de dtoa.c
David Gay aquí para ver un ejemplo de lo que implica una conversión de doble a cadena correctamente redondeada y de cadena a doble. (Python actualmente usa una variante de este código para sus conversiones de float a string y de string a flotación).
Incluso el estándar actual IEEE 754 para aritmética de punto flotante recomienda , pero no requiere que las conversiones de los tipos binarios de coma flotante a cadenas decimales siempre se redondeen correctamente. Aquí hay un fragmento, de la sección 5.12.2, "Secuencias de caracteres decimales externas que representan números finitos".
Puede haber un límite definido por la implementación en la cantidad de dígitos significativos que se pueden convertir con el redondeo correcto hacia y desde formatos binarios admitidos. Ese límite, H, será tal que H ≥ M + 3 y debería ser que H no tiene límites.
Aquí M
se define como el máximo de Pmin(bf)
sobre todos los formatos binarios admitidos bf
, y como Pmin(float64)
se define como 17
y .NET admite el formato float64 a través del tipo Double
, M
debería ser al menos 17
en .NET . En resumen, esto significa que si .NET siguiera el estándar, proporcionaría conversiones de cadenas redondeadas correctamente hasta al menos 20 dígitos significativos. Parece que el .NET Double
no cumple con este estándar.
En respuesta a la pregunta ''¿Es esto un error'', por mucho que me gustaría que fuera un error, realmente no parece haber ningún reclamo de precisión o conformidad con IEEE 754 en ningún lugar que pueda encontrar en la documentación del formato de número para .NET. Por lo tanto, podría considerarse indeseable, pero me sería difícil llamarlo un error real.
EDITAR: Jeppe Stig Nielsen señala que la página System.Double en MSDN establece que
Doble cumple con el estándar IEC 60559: 1989 (IEEE 754) para la aritmética binaria en coma flotante.
No está claro para mí exactamente qué se supone que cubre esta declaración de cumplimiento, pero incluso para la versión anterior de 1985 de IEEE 754, la conversión de cadena descrita parece violar los requisitos binarios a decimales de ese estándar.
Dado eso, felizmente actualizaré mi evaluación a ''posible error''.
Primero eche un vistazo a la parte inferior de esta página que muestra un problema similar de "doble redondeo".
Verificar la representación binaria / hexagonal de los siguientes números de punto flotante muestra que el rango dado se almacena como el mismo número en formato doble:
31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e
Como lo señalaron varios otros, eso se debe a que el doble representable más cercano tiene un valor exacto de 31.00000000000004973799150320701301097869873046875.
Hay dos aspectos adicionales a considerar en la conversión directa e inversa de IEEE 754 a cadenas, especialmente en el entorno .NET.
Primero (no puedo encontrar una fuente primaria) de Wikipedia tenemos:
Si una cadena decimal con un máximo de 15 decimales significativos se convierte a doble precisión IEEE 754 y luego se vuelve a convertir al mismo número de decimal significativo, entonces la cadena final debe coincidir con el original; y si una doble precisión IEEE 754 se convierte en una secuencia decimal con al menos 17 decimales significativos y luego se convierte de nuevo a doble, entonces el número final debe coincidir con el original.
Por lo tanto, en relación con el cumplimiento de la norma, convertir una cadena 31.0000000000000497 a doble no será necesariamente la misma cuando se vuelva a convertir en cadena (se han asignado demasiados decimales).
La segunda consideración es que, a menos que la conversión de doble cadena tenga 17 dígitos significativos, su comportamiento de redondeo no está explícitamente definido en el estándar.
Además, la documentación en Double.ToString () muestra que se rige por el especificador de formato numérico de la configuración cultural actual.
Explicación posible completa:
Sospecho que el doble redondeo ocurre de la siguiente manera: la secuencia decimal inicial se crea a 16 o 17 dígitos significativos porque esa es la precisión requerida para la conversión de "ida y vuelta" que da un resultado intermedio de 31.00000000000005 o 31.000000000000050. Luego, debido a la configuración de cultivo predeterminada, el resultado se redondea a 15 dígitos significativos, 31.00000000000001, porque 15 dígitos significativos decimales es la precisión mínima para todos los dobles.
Hacer una conversión intermedia a decimal, por otro lado, evita este problema de una manera diferente: trunca a 15 dígitos significativos directamente.
Tengo una sospecha más simple: el culpable es probablemente el operador de pow => **; Mientras que su número es exactamente representable como un doble, por razones de conveniencia (el operador de energía necesita mucho trabajo para trabajar correctamente) la potencia se calcula mediante la función exponencial. Esta es una razón por la que puede optimizar el rendimiento multiplicando un número repetidamente en lugar de usar pow () porque pow () es muy costoso.
Por lo tanto, no le da los 2 ^ 48 correctos, sino algo ligeramente incorrecto y, por lo tanto, tiene sus problemas de redondeo. Por favor revisa qué devuelve exactamente 2 ^ 48.
EDITAR: Perdón, hice solo un análisis del problema y arrojé una sospecha incorrecta. Existe un problema conocido con doble redondeo en los procesadores Intel. El código anterior usa el formato interno de 80 bits de la FPU en lugar de las instrucciones SSE que probablemente causen el error. El valor se escribe exactamente en el registro de 80 bits y luego se redondea dos veces , por lo que Jeppe ya ha encontrado y explicado claramente el problema.
Es un error ? Bueno, el procesador está haciendo todo bien, es simplemente el problema de que la FPU de Intel internamente tiene más precisión para las operaciones de coma flotante.
MÁS INFORMACIÓN Y EDITACIÓN: El "doble redondeo" es un problema conocido y se menciona explícitamente en "Manual de aritmética de punto flotante" de Jean-Michel Muller et. Alabama. en el capítulo "La necesidad de una revisión" en "3.3.1 Un problema típico:" doble redondeo "en la página 75:
El procesador que se utiliza puede ofrecer una precisión interna que es más amplia que la precisión de las variables del programa (un ejemplo típico es el formato de doble extensión disponible en las plataformas Intel, cuando las variables del programa son de precisión única o doble precisión Números de punto flotante). Esto a veces puede tener efectos secundarios extraños, como veremos en esta sección. Considere el programa C [...]
#include <stdio.h>
int main(void)
{
double a = 1848874847.0;
double b = 19954562207.0;
double c;
c = a * b;
printf("c = %20.19e/n", c);
return 0;
}
32 bits: GCC 4.1.2 20061115 en Linux / Debian
Con Compilerswitch o con -mfpmath = 387 (80bit-FPU): 3.6893488147419103232e + 19 -march = pentium4 -mfpmath = sse (SSE) o 64-bit: 3.6893488147419111424e + 19
Como se explica en el libro, la solución para la discrepancia es el doble redondeo con 80 bits y 53 bits.