Récemment, j’ai dû sérialiser un double en texte, puis le récupérer. La valeur ne semble pas être équivalente:
double d1 = 0.84551240822557006; ssortingng s = d1.ToSsortingng("R"); double d2 = double.Parse(s); bool s1 = d1 == d2; // -> s1 is False
Mais selon MSDN: Standard Numeric Format Ssortingngs , l’option “R” est censée garantir la sécurité aller-retour.
Le spécificateur de format aller-retour (“R”) est utilisé pour garantir qu’une valeur numérique convertie en chaîne sera analysée dans la même valeur numérique.
Pourquoi est-ce arrivé?
J’ai trouvé le bug.
.NET effectue les opérations suivantes dans 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 = NumberToSsortingng(&number, 'G', DOUBLE_PRECISION, gc.numfmt); goto lExit; } DoubleToNumber(value, 17, &number);
DoubleToNumber
est assez simple – il appelle simplement _ecvt
, qui est dans le runtime 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; } }
Il s’avère que _ecvt
renvoie la chaîne 845512408225570
.
Notez le zéro final? Il s’avère que ça fait toute la différence!
Lorsque le zéro est présent, le résultat parsing en réalité à 0.84551240822557006
, qui est votre numéro d’ origine – il est donc égal, et seulement 15 chiffres sont renvoyés.
Cependant, si je tronque la chaîne à ce zéro à 84551240822557
, alors je récupère 0.84551240822556994
, qui n’est pas votre numéro d’origine, et par conséquent il renverrait 17 chiffres.
Preuve: exécutez le code 64 bits suivant (extrait pour la plupart de la CLI 2.0 de Microsoft Shared Source) dans votre débogueur et examinez v
à la fin de main
:
#include #include #include #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; }
Il me semble que c’est simplement un bug. Vos attentes sont tout à fait raisonnables. Je l’ai reproduit en utilisant .NET 4.5.1 (x64), en exécutant l’application de console suivante qui utilise ma classe DoubleConverter
. DoubleConverter.ToExactSsortingng
affiche la valeur exacte représentée par un double
:
using System; class Test { static void Main() { double d1 = 0.84551240822557006; ssortingng s = d1.ToSsortingng("r"); double d2 = double.Parse(s); Console.WriteLine(s); Console.WriteLine(DoubleConverter.ToExactSsortingng(d1)); Console.WriteLine(DoubleConverter.ToExactSsortingng(d2)); Console.WriteLine(d1 == d2); } }
Résultats dans .NET:
0.84551240822557 0.845512408225570055719799711368978023529052734375 0.84551240822556994469749724885332398116588592529296875 False
Résultats en Mono 3.3.0:
0.84551240822557006 0.845512408225570055719799711368978023529052734375 0.845512408225570055719799711368978023529052734375 True
Si vous spécifiez manuellement la chaîne de Mono (qui contient le “006” à la fin), .NET parsingra cette valeur à la valeur d’origine. Il semble que le problème réside dans la gestion de ToSsortingng("R")
plutôt que dans l’parsing.
Comme indiqué dans d’autres commentaires, il semble que cela soit spécifique à l’exécution sous le CLR x64. Si vous comstackz et exécutez le code ci-dessus ciblant x86, c’est bien:
csc /platform:x86 Test.cs DoubleConverter.cs
… vous obtenez les mêmes résultats qu’avec Mono. Il serait intéressant de savoir si le bogue apparaît sous RyuJIT – je ne l’ai pas installé pour le moment. En particulier, je peux imaginer que cela puisse être un bug JIT, ou il est fort possible qu’il y ait des implémentations entières différentes des double.ToSsortingng
internes de double.ToSsortingng
basés sur l’architecture.
Je vous suggère de déposer un bug sur http://connect.microsoft.com
Récemment, j’essaie de résoudre ce problème . Comme indiqué dans le code , le double.ToSsortingng (“R”) a la logique suivante:
- Essayez de convertir le double en chaîne avec une précision de 15.
- Convertissez la chaîne en double pour la comparer au double d’origine. Si elles sont identiques, nous retournons la chaîne convertie dont la précision est 15.
- Sinon, convertissez le double en chaîne avec une précision de 17.
Dans ce cas, double.ToSsortingng (“R”) a choisi à tort le résultat avec une précision de 15, de sorte que le bogue se produit. Il y a une solution de rechange officielle dans le document MSDN:
Dans certains cas, les valeurs doubles formatées avec la chaîne de format numérique standard “R” ne peuvent pas effectuer l’aller-retour si elles sont compilées à l’aide des commutateurs / platform: x64 ou / platform: anycpu et s’exécutent sur des systèmes 64 bits. Pour contourner ce problème, vous pouvez mettre en forme des valeurs doubles à l’aide de la chaîne de format numérique standard “G17”. L’exemple suivant utilise la chaîne de format “R” avec une valeur Double qui ne fait pas un aller-retour réussi et utilise également la chaîne de format “G17” pour effectuer un aller-retour avec succès avec la valeur d’origine.
Donc, à moins que ce problème ne soit résolu, vous devez utiliser double.ToSsortingng (“G17”) pour le contournement.
Mise à jour : Il existe maintenant un problème spécifique pour suivre ce bogue.
Wow – une question de 3 ans et tout le monde semble avoir manqué un point – même Jon Skeet! (@ Jon: Respect. J’espère que je ne me ridiculise pas.)
Pour la petite histoire, j’ai exécuté l’exemple de code et dans mon environnement (Win10 x64 AnyCPU Debug, cible .NetFx 4.7), le test après un aller-retour a renvoyé la valeur true.
Voici une expérience Les chiffres sont alignés pour aider à faire le point …
Ce code …
ssortingng Breakdown(double v) { var ret = new SsortingngBuilder(); foreach (byte b in BitConverter.GetBytes(v)) ret.Append($"{b:X2} "); ret.Length--; return ret.ToSsortingng(); } { 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; } }
Produit cette sortie (les astérisques *** ont été ajoutés aprè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
Cela se fait artificiellement mais dans la 1ère section, la boucle compte par incréments de décimal 0.0000000000000001.
Remarquez comment deux "valeurs consécutives" (***) ont la même représentation binary interne.
Dans la deuxième partie - parce que nous ne sautons pas à travers les cerceaux pour forcer l'addition décimale - la valeur interne ne cesse de grimper sur le bit le moins significatif. Les deux séquences de 10 valeurs ne sont plus synchronisées après 5 itérations.
Le point est que les doubles (en interne binarys) ne peuvent pas avoir des représentations décimales exactes et vice-versa.
Nous ne pouvons qu'essayer d'obtenir une chaîne décimale représentant notre valeur "aussi proche que possible".
Ici, la chaîne au format R 0.99999999999999745 est ambiguë "plus proche de" soit 0.9999999999999974 ou 0.9999999999999975.
J'apprécie le fait que la question semble "montrer cette fonctionnalité dans l'autre sens" (une représentation décimale correspondant de manière ambiguë à deux binarys différents), mais elle n'a pas réussi à la recréer.
Après tout, nous sums à la limite de la précision des doubles et c'est pourquoi des chaînes de format R sont nécessaires.
J'aime bien penser de cette façon "Le spécificateur de format aller-retour produit une chaîne représentant la valeur double la plus proche de votre valeur double qui peut être arrondie. " En d’autres termes, "la chaîne formatée R doit être aller-retour". capable, pas nécessairement la valeur. "
Pour travailler le point, il ne faut pas supposer que "value -> ssortingng -> même valeur" est possible mais
devrait pouvoir compter sur "value -> ssortingng -> valeur voisine -> même chaîne -> même valeur proche -> ...
Rappelles toi
La représentation interne des doubles dépend de l'environnement / de la plateforme
Même dans un écosystème entièrement Microsoft, il existe encore de nombreuses variantes possibles
une. Options de compilation (x86 / x64 / AnyCPU, Release / Debug)
b. Matériel (les processeurs Intel ont un registre de 80 bits pour l'arithmétique - qui pourrait être utilisé différemment par le code de construction de débogage et de publication)
c. Qui sait où le code IL pourrait fonctionner (mode 32 bits sous 64 bits sur le système d'exploitation X / Y, etc.)?
Cela devrait "réparer" le code de la question originale ...
double d1 = 0.84551240822557006; ssortingng s1 = d1.ToSsortingng("R"); double d2 = double.Parse(s1); // d2 is not necessarily == d1 ssortingng s2 = d2.ToSsortingng("R"); double d3 = double.Parse(s2); // you must get true here bool roundTripSuccess = d2 == d3;