visual valid net method example documentacion comment comentarios code c# double tostring precision

valid - comments code c#



¿Por qué una conversión de ida y vuelta a través de una cadena no es segura para un doble? (4)

Recientemente tuve que serializar un doble en un texto y luego recuperarlo. El valor parece no ser equivalente:

double d1 = 0.84551240822557006; string s = d1.ToString("R"); double d2 = double.Parse(s); bool s1 = d1 == d2; // -> s1 is False

Pero según MSDN: cadenas de formato numérico estándar , se supone que la opción "R" garantiza la seguridad de ida y vuelta.

El especificador de formato de ida y vuelta ("R") se utiliza para garantizar que un valor numérico que se convierte en una cadena se vuelva a analizar en el mismo valor numérico

¿Por qué pasó esto?


Encontré el error.

.NET hace lo siguiente en clr/src/vm/comnumber.cpp :

DoubleToNumber(value, DOUBLE_PRECISION, &number); if (number.scale == (int) SCALE_NAN) { gc.refRetVal = gc.numfmt->sNaN; goto lExit; } if (number.scale == SCALE_INF) { gc.refRetVal = (number.sign? gc.numfmt->sNegativeInfinity: gc.numfmt->sPositiveInfinity); goto lExit; } NumberToDouble(&number, &dTest); if (dTest == value) { gc.refRetVal = NumberToString(&number, ''G'', DOUBLE_PRECISION, gc.numfmt); goto lExit; } DoubleToNumber(value, 17, &number);

DoubleToNumber es bastante simple: simplemente llama a _ecvt , que está en el tiempo de ejecución de C:

void DoubleToNumber(double value, int precision, NUMBER* number) { WRAPPER_CONTRACT _ASSERTE(number != NULL); number->precision = precision; if (((FPDOUBLE*)&value)->exp == 0x7FF) { number->scale = (((FPDOUBLE*)&value)->mantLo || ((FPDOUBLE*)&value)->mantHi) ? SCALE_NAN: SCALE_INF; number->sign = ((FPDOUBLE*)&value)->sign; number->digits[0] = 0; } else { char* src = _ecvt(value, precision, &number->scale, &number->sign); wchar* dst = number->digits; if (*src != ''0'') { while (*src) *dst++ = *src++; } *dst = 0; } }

Resulta que _ecvt devuelve la cadena 845512408225570 .

Observe el cero al final? ¡Resulta que hace toda la diferencia!
Cuando el cero está presente, el resultado realmente analiza de nuevo a 0.84551240822557006 , que es su número original , por lo que se compara igual, y por lo tanto, solo se devuelven 15 dígitos.

Sin embargo, si trunco ​​la cadena en ese cero a 84551240822557 , obtengo 0.84551240822556994 , que no es su número original, y por lo tanto devolvería 17 dígitos.

Prueba: ejecute el siguiente código de 64 bits (la mayoría de los cuales extraje de Microsoft Shared Source CLI 2.0) en su depurador y examine v al final de main :

#include <stdlib.h> #include <string.h> #include <math.h> #define min(a, b) (((a) < (b)) ? (a) : (b)) struct NUMBER { int precision; int scale; int sign; wchar_t digits[20 + 1]; NUMBER() : precision(0), scale(0), sign(0) {} }; #define I64(x) x##LL static const unsigned long long rgval64Power10[] = { // powers of 10 /*1*/ I64(0xa000000000000000), /*2*/ I64(0xc800000000000000), /*3*/ I64(0xfa00000000000000), /*4*/ I64(0x9c40000000000000), /*5*/ I64(0xc350000000000000), /*6*/ I64(0xf424000000000000), /*7*/ I64(0x9896800000000000), /*8*/ I64(0xbebc200000000000), /*9*/ I64(0xee6b280000000000), /*10*/ I64(0x9502f90000000000), /*11*/ I64(0xba43b74000000000), /*12*/ I64(0xe8d4a51000000000), /*13*/ I64(0x9184e72a00000000), /*14*/ I64(0xb5e620f480000000), /*15*/ I64(0xe35fa931a0000000), // powers of 0.1 /*1*/ I64(0xcccccccccccccccd), /*2*/ I64(0xa3d70a3d70a3d70b), /*3*/ I64(0x83126e978d4fdf3c), /*4*/ I64(0xd1b71758e219652e), /*5*/ I64(0xa7c5ac471b478425), /*6*/ I64(0x8637bd05af6c69b7), /*7*/ I64(0xd6bf94d5e57a42be), /*8*/ I64(0xabcc77118461ceff), /*9*/ I64(0x89705f4136b4a599), /*10*/ I64(0xdbe6fecebdedd5c2), /*11*/ I64(0xafebff0bcb24ab02), /*12*/ I64(0x8cbccc096f5088cf), /*13*/ I64(0xe12e13424bb40e18), /*14*/ I64(0xb424dc35095cd813), /*15*/ I64(0x901d7cf73ab0acdc), }; static const signed char rgexp64Power10[] = { // exponents for both powers of 10 and 0.1 /*1*/ 4, /*2*/ 7, /*3*/ 10, /*4*/ 14, /*5*/ 17, /*6*/ 20, /*7*/ 24, /*8*/ 27, /*9*/ 30, /*10*/ 34, /*11*/ 37, /*12*/ 40, /*13*/ 44, /*14*/ 47, /*15*/ 50, }; static const unsigned long long rgval64Power10By16[] = { // powers of 10^16 /*1*/ I64(0x8e1bc9bf04000000), /*2*/ I64(0x9dc5ada82b70b59e), /*3*/ I64(0xaf298d050e4395d6), /*4*/ I64(0xc2781f49ffcfa6d4), /*5*/ I64(0xd7e77a8f87daf7fa), /*6*/ I64(0xefb3ab16c59b14a0), /*7*/ I64(0x850fadc09923329c), /*8*/ I64(0x93ba47c980e98cde), /*9*/ I64(0xa402b9c5a8d3a6e6), /*10*/ I64(0xb616a12b7fe617a8), /*11*/ I64(0xca28a291859bbf90), /*12*/ I64(0xe070f78d39275566), /*13*/ I64(0xf92e0c3537826140), /*14*/ I64(0x8a5296ffe33cc92c), /*15*/ I64(0x9991a6f3d6bf1762), /*16*/ I64(0xaa7eebfb9df9de8a), /*17*/ I64(0xbd49d14aa79dbc7e), /*18*/ I64(0xd226fc195c6a2f88), /*19*/ I64(0xe950df20247c83f8), /*20*/ I64(0x81842f29f2cce373), /*21*/ I64(0x8fcac257558ee4e2), // powers of 0.1^16 /*1*/ I64(0xe69594bec44de160), /*2*/ I64(0xcfb11ead453994c3), /*3*/ I64(0xbb127c53b17ec165), /*4*/ I64(0xa87fea27a539e9b3), /*5*/ I64(0x97c560ba6b0919b5), /*6*/ I64(0x88b402f7fd7553ab), /*7*/ I64(0xf64335bcf065d3a0), /*8*/ I64(0xddd0467c64bce4c4), /*9*/ I64(0xc7caba6e7c5382ed), /*10*/ I64(0xb3f4e093db73a0b7), /*11*/ I64(0xa21727db38cb0053), /*12*/ I64(0x91ff83775423cc29), /*13*/ I64(0x8380dea93da4bc82), /*14*/ I64(0xece53cec4a314f00), /*15*/ I64(0xd5605fcdcf32e217), /*16*/ I64(0xc0314325637a1978), /*17*/ I64(0xad1c8eab5ee43ba2), /*18*/ I64(0x9becce62836ac5b0), /*19*/ I64(0x8c71dcd9ba0b495c), /*20*/ I64(0xfd00b89747823938), /*21*/ I64(0xe3e27a444d8d991a), }; static const signed short rgexp64Power10By16[] = { // exponents for both powers of 10^16 and 0.1^16 /*1*/ 54, /*2*/ 107, /*3*/ 160, /*4*/ 213, /*5*/ 266, /*6*/ 319, /*7*/ 373, /*8*/ 426, /*9*/ 479, /*10*/ 532, /*11*/ 585, /*12*/ 638, /*13*/ 691, /*14*/ 745, /*15*/ 798, /*16*/ 851, /*17*/ 904, /*18*/ 957, /*19*/ 1010, /*20*/ 1064, /*21*/ 1117, }; static unsigned DigitsToInt(wchar_t* p, int count) { wchar_t* end = p + count; unsigned res = *p - ''0''; for ( p = p + 1; p < end; p++) { res = 10 * res + *p - ''0''; } return res; } #define Mul32x32To64(a, b) ((unsigned long long)((unsigned long)(a)) * (unsigned long long)((unsigned long)(b))) static unsigned long long Mul64Lossy(unsigned long long a, unsigned long long b, int* pexp) { // it''s ok to losse some precision here - Mul64 will be called // at most twice during the conversion, so the error won''t propagate // to any of the 53 significant bits of the result unsigned long long val = Mul32x32To64(a >> 32, b >> 32) + (Mul32x32To64(a >> 32, b) >> 32) + (Mul32x32To64(a, b >> 32) >> 32); // normalize if ((val & I64(0x8000000000000000)) == 0) { val <<= 1; *pexp -= 1; } return val; } void NumberToDouble(NUMBER* number, double* value) { unsigned long long val; int exp; wchar_t* src = number->digits; int remaining; int total; int count; int scale; int absscale; int index; total = (int)wcslen(src); remaining = total; // skip the leading zeros while (*src == ''0'') { remaining--; src++; } if (remaining == 0) { *value = 0; goto done; } count = min(remaining, 9); remaining -= count; val = DigitsToInt(src, count); if (remaining > 0) { count = min(remaining, 9); remaining -= count; // get the denormalized power of 10 unsigned long mult = (unsigned long)(rgval64Power10[count-1] >> (64 - rgexp64Power10[count-1])); val = Mul32x32To64(val, mult) + DigitsToInt(src+9, count); } scale = number->scale - (total - remaining); absscale = abs(scale); if (absscale >= 22 * 16) { // overflow / underflow *(unsigned long long*)value = (scale > 0) ? I64(0x7FF0000000000000) : 0; goto done; } exp = 64; // normalize the mantisa if ((val & I64(0xFFFFFFFF00000000)) == 0) { val <<= 32; exp -= 32; } if ((val & I64(0xFFFF000000000000)) == 0) { val <<= 16; exp -= 16; } if ((val & I64(0xFF00000000000000)) == 0) { val <<= 8; exp -= 8; } if ((val & I64(0xF000000000000000)) == 0) { val <<= 4; exp -= 4; } if ((val & I64(0xC000000000000000)) == 0) { val <<= 2; exp -= 2; } if ((val & I64(0x8000000000000000)) == 0) { val <<= 1; exp -= 1; } index = absscale & 15; if (index) { int multexp = rgexp64Power10[index-1]; // the exponents are shared between the inverted and regular table exp += (scale < 0) ? (-multexp + 1) : multexp; unsigned long long multval = rgval64Power10[index + ((scale < 0) ? 15 : 0) - 1]; val = Mul64Lossy(val, multval, &exp); } index = absscale >> 4; if (index) { int multexp = rgexp64Power10By16[index-1]; // the exponents are shared between the inverted and regular table exp += (scale < 0) ? (-multexp + 1) : multexp; unsigned long long multval = rgval64Power10By16[index + ((scale < 0) ? 21 : 0) - 1]; val = Mul64Lossy(val, multval, &exp); } // round & scale down if ((unsigned long)val & (1 << 10)) { // IEEE round to even unsigned long long tmp = val + ((1 << 10) - 1) + (((unsigned long)val >> 11) & 1); if (tmp < val) { // overflow tmp = (tmp >> 1) | I64(0x8000000000000000); exp += 1; } val = tmp; } val >>= 11; exp += 0x3FE; if (exp <= 0) { if (exp <= -52) { // underflow val = 0; } else { // denormalized val >>= (-exp+1); } } else if (exp >= 0x7FF) { // overflow val = I64(0x7FF0000000000000); } else { val = ((unsigned long long)exp << 52) + (val & I64(0x000FFFFFFFFFFFFF)); } *(unsigned long long*)value = val; done: if (number->sign) *(unsigned long long*)value |= I64(0x8000000000000000); } int main() { NUMBER number; number.precision = 15; double v = 0.84551240822557006; char *src = _ecvt(v, number.precision, &number.scale, &number.sign); int truncate = 0; // change to 1 if you want to truncate if (truncate) { while (*src && src[strlen(src) - 1] == ''0'') { src[strlen(src) - 1] = 0; } } wchar_t* dst = number.digits; if (*src != ''0'') { while (*src) *dst++ = *src++; } *dst++ = 0; NumberToDouble(&number, &v); return 0; }


Guau, una pregunta de 3 años y todos parecen haber perdido un punto, ¡incluso Jon Skeet! (@Jon: Respeto. Espero no estar haciendo el ridículo.)

Para el registro, ejecuté el ejemplo de código y en mi entorno (Depuración AnyU de Win10 x64, destino .NetFx 4.7) la prueba después del viaje de ida y vuelta devolvió verdadero.

Aquí hay un experimento. Los dígitos están alineados para ayudar a que el punto ...

Este código ...

string Breakdown(double v) { var ret = new StringBuilder(); foreach (byte b in BitConverter.GetBytes(v)) ret.Append($"{b:X2} "); ret.Length--; return ret.ToString(); } { var start = "0.99999999999999"; var incr = 70; for (int i = 0; i < 10; i++) { var dblStr = start + incr.ToString(); var dblVal = double.Parse(dblStr); Console.WriteLine($"{dblStr} : {dblVal:N16} : {Breakdown(dblVal)} : {dblVal:R}"); incr++; } } Console.WriteLine(); { var start = 0.999999999999997; var incr = 0.0000000000000001; var dblVal = start; for (int i = 0; i < 10; i++) { Console.WriteLine($"{i,-18} : {dblVal:N16} : {Breakdown(dblVal)} : {dblVal:R}"); dblVal += incr; } }

Produce esta salida (los asteriscos *** se agregaron después) ...

0.9999999999999970 : 0.9999999999999970 : E5 FF FF FF FF FF EF 3F : 0.999999999999997 0.9999999999999971 : 0.9999999999999970 : E6 FF FF FF FF FF EF 3F : 0.99999999999999711 0.9999999999999972 : 0.9999999999999970 : E7 FF FF FF FF FF EF 3F : 0.99999999999999722 0.9999999999999973 : 0.9999999999999970 : E8 FF FF FF FF FF EF 3F : 0.99999999999999734 *** 0.9999999999999974 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 *** 0.9999999999999975 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 0.9999999999999976 : 0.9999999999999980 : EA FF FF FF FF FF EF 3F : 0.99999999999999756 0.9999999999999977 : 0.9999999999999980 : EB FF FF FF FF FF EF 3F : 0.99999999999999767 0.9999999999999978 : 0.9999999999999980 : EC FF FF FF FF FF EF 3F : 0.99999999999999778 0.9999999999999979 : 0.9999999999999980 : ED FF FF FF FF FF EF 3F : 0.99999999999999789 0 : 0.9999999999999970 : E5 FF FF FF FF FF EF 3F : 0.999999999999997 1 : 0.9999999999999970 : E6 FF FF FF FF FF EF 3F : 0.99999999999999711 2 : 0.9999999999999970 : E7 FF FF FF FF FF EF 3F : 0.99999999999999722 3 : 0.9999999999999970 : E8 FF FF FF FF FF EF 3F : 0.99999999999999734 +++ 4 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 5 : 0.9999999999999980 : EA FF FF FF FF FF EF 3F : 0.99999999999999756 6 : 0.9999999999999980 : EB FF FF FF FF FF EF 3F : 0.99999999999999767 7 : 0.9999999999999980 : EC FF FF FF FF FF EF 3F : 0.99999999999999778 8 : 0.9999999999999980 : ED FF FF FF FF FF EF 3F : 0.99999999999999789 9 : 0.9999999999999980 : EE FF FF FF FF FF EF 3F : 0.999999999999998

Se realiza de forma artificial, pero en la primera sección el ciclo cuenta en incrementos de 0.0000000000000001 decimales.
Observe cómo dos "valores consecutivos" (***) tienen la misma representación binaria interna.

En la segunda sección, porque no estamos saltando a través de aros para forzar la suma decimal, el valor interno sigue aumentando en el bit menos significativo. Las dos secuencias de 10 valores se desincronizan después de 5 iteraciones.

El punto es que los dobles (internamente binarios) no pueden tener representaciones decimales exactas y viceversa.
Solo podemos tratar de obtener una cadena decimal que represente nuestro valor "lo más cerca posible".
Aquí la cadena con formato R 0.99999999999999745 es ambiguamente "más cercana a" 0.999999999999999974 o 0.9999999999999975.

Aprecio que la pregunta parezca "mostrar esta característica al revés" (un mapeo de representación decimal ambigua a dos binarios diferentes) pero no he logrado recrear eso.
Después de todo, estamos en el límite de la precisión de los dobles, y es por eso que se necesitan cadenas con formato R.

Me gusta pensarlo de esta manera "El especificador de formato de ida y vuelta produce una cadena que representa el valor doble más cercano a su valor doble que se puede disparar en redondo. " En otras palabras, "la cadena con formato R debe ser de ida y vuelta- capaz, no necesariamente el valor ".

Para trabajar el punto, uno no debe suponer que "valor -> cadena -> mismo valor" es posible, pero
debería ser capaz de confiar en "valor -> cadena -> valor cercano -> misma cadena -> mismo valor cercano -> ...

Recuerda

  1. La representación interna de dobles depende del entorno / plataforma

  2. Incluso en un ecosistema completamente de Microsoft todavía hay muchas variaciones posibles

    a. Opciones de compilación (x86 / x64 / AnyCPU, Release / Debug)

    segundo. Hardware (las CPU Intel tienen el registro de aritmética de 80 bits, que podría usarse de forma diferente mediante el código de compilación de depuración y liberación)

    do. ¿Quién sabe dónde podría encontrarse el código IL (modo de 32 bits en 64 bits en el sistema operativo X / Y, etc.)?

Esto debería "arreglar" el código de la pregunta original ...

double d1 = 0.84551240822557006; string s1 = d1.ToString("R"); double d2 = double.Parse(s1); // d2 is not necessarily == d1 string s2 = d2.ToString("R"); double d3 = double.Parse(s2); // you must get true here bool roundTripSuccess = d2 == d3;


Me parece que esto es simplemente un error. Tus expectativas son completamente razonables. Lo he reproducido usando .NET 4.5.1 (x64), ejecutando la siguiente aplicación de consola que usa mi clase DoubleConverter . DoubleConverter.ToExactString muestra el valor exacto representado por un double :

using System; class Test { static void Main() { double d1 = 0.84551240822557006; string s = d1.ToString("r"); double d2 = double.Parse(s); Console.WriteLine(s); Console.WriteLine(DoubleConverter.ToExactString(d1)); Console.WriteLine(DoubleConverter.ToExactString(d2)); Console.WriteLine(d1 == d2); } }

Resultados en .NET:

0.84551240822557 0.845512408225570055719799711368978023529052734375 0.84551240822556994469749724885332398116588592529296875 False

Resultados en Mono 3.3.0:

0.84551240822557006 0.845512408225570055719799711368978023529052734375 0.845512408225570055719799711368978023529052734375 True

Si especifica manualmente la cadena desde Mono (que contiene el "006" al final), .NET lo analizará de nuevo al valor original. Parece que el problema está en el ToString("R") lugar del análisis sintáctico.

Como se señaló en otros comentarios, parece que esto es específico de correr bajo x64 CLR. Si compila y ejecuta el código anterior que apunta a x86, está bien:

csc /platform:x86 Test.cs DoubleConverter.cs

... obtienes los mismos resultados que con Mono. Sería interesante saber si el error aparece bajo RyuJIT. No tengo eso instalado en este momento. En particular, puedo imaginar que posiblemente sea ​​un error JIT, o es muy posible que haya implementaciones completamente diferentes de las double.ToString internas de double.ToString basadas en la arquitectura.

Sugiero que presente un error en http://connect.microsoft.com


Recientemente, estoy tratando de resolver este problema . Como se señala a través del código , double.ToString ("R") tiene la siguiente lógica:

  1. Intenta convertir el doble en cuerda con una precisión de 15.
  2. Convierta la cadena de nuevo a doble y compárela con el doble original. Si son iguales, devolvemos la cadena convertida cuya precisión es 15.
  3. De lo contrario, convierta el doble en cuerda con una precisión de 17.

En este caso, double.ToString ("R") eligió erróneamente el resultado en una precisión de 15 para que ocurra el error. Hay una solución oficial en el documento de MSDN:

En algunos casos, los valores dobles formateados con la cadena de formato numérico estándar "R" no realizan un viaje de ida y vuelta si se compilan utilizando / plataforma: x64 o / plataforma: los conmutadores anycpu se ejecutan en sistemas de 64 bits. Para evitar este problema, puede formatear valores dobles utilizando la cadena de formato numérico estándar "G17". El siguiente ejemplo utiliza la cadena de formato "R" con un valor doble que no realiza un viaje de ida y vuelta con éxito, y también utiliza la cadena de formato "G17" para realizar un viaje de ida y vuelta con éxito del valor original.

Por lo tanto, a menos que se resuelva este problema, debe usar double.ToString ("G17") para el disparo circular.

Actualización : ahora hay un problema específico para rastrear este error.