Pregunta

Ejecutar un experimento rápido relacionado con ¿Se rompe la multiplicación doble en .NET? y leyendo un par de artículos sobre el formato de cadena C #, pensé que esto:

{
    double i = 10 * 0.69;
    Console.WriteLine(i);
    Console.WriteLine(String.Format("  {0:F20}", i));
    Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
    Console.WriteLine(String.Format("= {0:F20}", 6.9));
}

Sería el equivalente en C # de este código C:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}

Sin embargo, C # produce la salida:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000

a pesar de que aparece igual al valor 6.89999999999999946709 (en lugar de 6.9) en el depurador.

en comparación con C, que muestra la precisión solicitada por el formato:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          

¿Qué está pasando?

(Microsoft .NET Framework Versión 3.51 SP1 / Visual Studio C # 2008 Express Edition)


Tengo experiencia en computación numérica y experiencia en la implementación de aritmética de intervalos, una técnica para estimar errores debido a los límites de precisión en sistemas numéricos complicados, en varias plataformas. Para obtener la recompensa, no intente explicar la precisión del almacenamiento; en este caso, es una diferencia de un ULP de un doble de 64 bits.

Para obtener la recompensa, quiero saber cómo (o si) .Net puede formatear un doble a la precisión solicitada como se ve en el código C.

¿Fue útil?

Solución

El problema es que .NET siempre redondeará un double a 15 dígitos decimales significativos antes de aplicar su formato, independientemente de la precisión solicitada por su formato e independientemente del valor decimal exacto del número binario.

Supongo que el depurador de Visual Studio tiene sus propias rutinas de formato / visualización que acceden directamente al número binario interno, de ahí las discrepancias entre su código C #, su código C y el depurador.

No hay nada incorporado que le permita acceder al valor decimal exacto de un double , o que le permita formatear un double a un número específico de decimal lugares, pero puede hacerlo usted mismo separando el número binario interno y reconstruyéndolo como una representación de cadena del valor decimal.

Alternativamente, puede utilizar la clase DoubleConverter de Jon Skeet (vinculado a desde su " Punto flotante binario y .NET " artículo ). Esto tiene un método ToExactString que devuelve el valor decimal exacto de un double . Puede modificar esto fácilmente para permitir el redondeo de la salida a una precisión específica.

double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));

// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625

Otros consejos

Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567);      // "123.46"
String.Format("{0:0.00}", 123.4);         // "123.40"
String.Format("{0:0.00}", 123.0);         // "123.00"

// max. two decimal places
String.Format("{0:0.##}", 123.4567);      // "123.46"
String.Format("{0:0.##}", 123.4);         // "123.4"
String.Format("{0:0.##}", 123.0);         // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567);      // "123.5"
String.Format("{0:00.0}", 23.4567);       // "23.5"
String.Format("{0:00.0}", 3.4567);        // "03.5"
String.Format("{0:00.0}", -3.4567);       // "-03.5"

Thousands separator
String.Format("{0:0,0.0}", 12345.67);     // "12,345.7"
String.Format("{0:0,0}", 12345.67);       // "12,346"

Zero
Following code shows how can be formatted a zero (of double type).

String.Format("{0:0.0}", 0.0);            // "0.0"
String.Format("{0:0.#}", 0.0);            // "0"
String.Format("{0:#.0}", 0.0);            // ".0"
String.Format("{0:#.#}", 0.0);            // ""

Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567);    // "     123.5"
String.Format("{0,-10:0.0}", 123.4567);   // "123.5     "
String.Format("{0,10:0.0}", -123.4567);   // "    -123.5"
String.Format("{0,-10:0.0}", -123.4567);  // "-123.5    "

Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567);   // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567);  // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0);        // "zero"

Some funny examples
String.Format("{0:my number is 0.0}", 12.3);   // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);

Eche un vistazo a esta referencia de MSDN . En las notas indica que los números se redondean a la cantidad de decimales solicitados.

Si en su lugar usa " {0: R} " producirá lo que se conoce como "ida y vuelta" valor, eche un vistazo a esta referencia de MSDN para obtener más información, Aquí está mi código y la salida:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

salida

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000

Aunque esta pregunta está cerrada mientras tanto, creo que vale la pena mencionar cómo surgió esta atrocidad. En cierto modo, puede culpar a la especificación de C #, que establece que un doble debe tener una precisión de 15 o 16 dígitos (el resultado de IEEE-754). Un poco más adelante (sección 4.1.6) se afirma que las implementaciones pueden usar una precisión mayor . Eso sí: más alto , no más bajo. Incluso se les permite desviarse de IEEE-754: expresiones del tipo x * y / z donde x * y produciría +/- INF pero estaría en un rango válido después de dividir, no tiene que resultar en un error. Esta característica facilita a los compiladores el uso de mayor precisión en arquitecturas donde eso produciría un mejor rendimiento.

Pero prometí una " razón " ;. Aquí hay una cotización (solicitó un recurso en uno de sus comentarios recientes) de CLI de origen compartido , en clr / src / vm / comnumber.cpp :

  

" Para dar números que son ambos   amigable para mostrar y   ida y vuelta, analizamos el número   usando 15 dígitos y luego determinar si   Es ida y vuelta al mismo valor. Si   lo hace, convertimos ese NÚMERO en un   cadena, de lo contrario volveremos a analizar usando 17   dígitos y mostrar eso. "

En otras palabras: el equipo de desarrollo de la CLI de MS decidió ser de ida y vuelta y mostrar valores bonitos que no son tan difíciles de leer. ¿Bueno o malo? Desearía una opción de inclusión o exclusión.

¿El truco que hace para descubrir esta ida y vuelta de cualquier número dado? Conversión a una estructura NUMBER genérica (que tiene campos separados para las propiedades de un doble) y viceversa, y luego compara si el resultado es diferente. Si es diferente, se usa el valor exacto (como en su valor medio con 6.9 - i ) si es el mismo, el " valor bonito " es usado

Como ya comentó en un comentario a Andyp, 6.90 ... 00 es bit a bit igual a 6.89 ... 9467 . Y ahora ya sabe por qué se usa 0.0 ... 8818 : es ligeramente diferente de 0.0 .

Esta barrera de 15 dígitos está codificada y solo se puede cambiar volviendo a compilar la CLI, utilizando Mono o llamando a Microsoft y convenciéndoles de que agreguen una opción para imprimir la precisión completa " (No es realmente precisión, sino por la falta de una palabra mejor). Probablemente sea más fácil calcular la precisión de 52 bits usted mismo o usar la biblioteca mencionada anteriormente.

EDITAR: si desea experimentar con puntos flotantes IEE-754, considere esta herramienta en línea , que le muestra todas las partes relevantes de un punto flotante.

Uso

Console.WriteLine(String.Format("  {0:G17}", i));

Eso te dará los 17 dígitos que tiene. Por defecto, un valor Doble contiene 15 dígitos decimales de precisión, aunque internamente se mantiene un máximo de 17 dígitos. {0: R} no siempre le dará 17 dígitos, dará 15 si el número puede representarse con esa precisión.

que devuelve 15 dígitos si el número puede representarse con esa precisión o 17 dígitos si el número solo puede representarse con la máxima precisión. No hay nada que puedas hacer para que el doble retorno tenga más dígitos, de esa forma se implementa. Si no te gusta, haz una nueva clase doble tú mismo ...

El doble de .NET no puede almacenar más dígitos que 17, por lo que no puede ver 6.89999999999999946709 en el depurador, verá 6.8999999999999995. Proporcione una imagen para demostrar que estamos equivocados.

La respuesta a esto es simple y se puede encontrar en MSDN

  

Recuerde que un número de coma flotante solo puede aproximarse a un número decimal, y que la precisión de un número de coma flotante determina la precisión con que ese número se aproxima a un número decimal. De forma predeterminada, un valor Doble contiene 15 dígitos decimales de precisión , aunque internamente se mantiene un máximo de 17 dígitos.

En su ejemplo, el valor de i es 6.89999999999999946709 que tiene el número 9 para todas las posiciones entre el 3er y el 16to dígito (recuerde contar la parte entera en los dígitos). Cuando se convierte en cadena, el marco redondea el número al décimo quinto dígito.

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901

Traté de reproducir sus hallazgos, pero cuando vi 'i' en el depurador, apareció como '6.8999999999999995' no como '6.89999999999999946709' como escribió en la pregunta. ¿Puede proporcionar pasos para reproducir lo que vio?

Para ver lo que le muestra el depurador, puede usar un DoubleConverter como en la siguiente línea de código:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

¡Espero que esto ayude!

Editar: supongo que estoy más cansado de lo que pensaba, por supuesto, esto es lo mismo que formatear el valor de ida y vuelta (como se mencionó anteriormente).

La respuesta es sí, la doble impresión está rota en .NET, están imprimiendo dígitos de basura finales.

Puede leer cómo implementarlo correctamente aquí .

He tenido que hacer lo mismo para IronScheme.

> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995

Nota: tanto C como C # tienen el valor correcto, solo impresión rota.

Actualización: Todavía estoy buscando la conversación de la lista de correo que tuve que condujo a este descubrimiento.

Encontré esta solución rápida.

    double i = 10 * 0.69;
    System.Diagnostics.Debug.WriteLine(i);


    String s = String.Format("{0:F20}", i).Substring(0,20);
    System.Diagnostics.Debug.WriteLine(s + " " +s.Length );

Console.WriteLine (string.Format (" Las tarifas del curso son {0: 0.00} " ;, + cfees));

internal void DisplaycourseDetails()
        {
            Console.WriteLine("Course Code : " + cid);
            Console.WriteLine("Couse Name : " + cname);
            //Console.WriteLine("Couse Name : " + string.Format("{0:0.00}"+ cfees));
            // string s = string.Format("Course Fees is {0:0.00}", +cfees);
            // Console.WriteLine(s);
            Console.WriteLine( string.Format("Course Fees is {0:0.00}", +cfees));
        }
        static void Main(string[] args)
        {
            Course obj1 = new Course(101, "C# .net", 1100.00);
            obj1.DisplaycourseDetails();
            Course obj2 = new Course(102, "Angular", 7000.00);
            obj2.DisplaycourseDetails();
            Course obj3 = new Course(103, "MVC", 1100.00);
            obj3.DisplaycourseDetails();
            Console.ReadLine();
        }
    }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top