为什么通过一个string往返转换不安全的双?

最近我不得不把序列化成文本,然后把它取回来。 价值似乎不是等值的:

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

但根据MSDN:标准数字格式string ,“R”选项应该保证往返安全。

往返(“R”)格式说明符用于确保转换为string的数值将被parsing回相同的数值

为什么会发生?

我发现了错误。

.NET在clr\src\vm\comnumber.cpp中执行以下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非常简单 – 它只是调用C运行库中的_ecvt

 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; } } 

事实certificate, _ecvt返回string845512408225570

注意尾随零? 事实certificate,所有的区别!
当零出现时,结果实际上parsing回0.84551240822557006 ,这是你的原始数字,所以它比较相等,因此只返回15位数字。

但是,如果我截断string在零到84551240822557 ,然后我回来0.84551240822556994 ,这不是你的原始数字,因此它将返回17位数字。

certificate:在debugging器中运行以下64位代码(其中大部分是从Microsoft Shared Source CLI 2.0中提取的),并在main的结尾处检查v

 #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; } 

在我看来,这只是一个错误。 你的期望是完全合理的。 我已经使用.NET 4.5.1(x64)复制它,运行使用我的DoubleConverter类的以下控制台应用程序。 DoubleConverter.ToExactString显示由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); } } 

.NET中的结果:

 0.84551240822557 0.845512408225570055719799711368978023529052734375 0.84551240822556994469749724885332398116588592529296875 False 

Mono 3.3.0中的结果:

 0.84551240822557006 0.845512408225570055719799711368978023529052734375 0.845512408225570055719799711368978023529052734375 True 

如果你手动指定Mono的string(在结尾包含“006”),.NET会将其parsing回原来的值。 看起来问题是在ToString("R")处理而不是parsing。

正如其他评论中指出的那样,它看起来像是在x64 CLR下运行。 如果你编译并运行上面的x86代码,那很好:

 csc /platform:x86 Test.cs DoubleConverter.cs 

…与Mono相同的结果。 知道这个bug是不是出现在RyuJIT下面会很有趣 – 我自己现在还没有安装。 特别是,我可以想象这可能是一个JIT错误,或者很可能有基于体系结构的double.ToString内部的完全不同的实现。

我build议你在http://connect.microsoft.com提交一个错误;

最近, 我试图解决这个问题 。 正如通过代码指出的那样,double.ToString(“R”)具有以下逻辑:

  1. 尝试将double转换为精度为15的string。
  2. 将string转换为double并与原始double进行比较。 如果它们相同,则返回精度为15的转换string。
  3. 否则,将double转换为精度为17的string。

在这种情况下,double.ToString(“R”)错误地select了精度为15的结果,所以会发生错误。 MSDN文档中有一个官方的解决方法:

在某些情况下,使用“R”标准数字格式string格式化的Double值如果使用/ platform:x64或/ platform:anycpu开关编译并在64位系统上运行,则不会成功执行往返。 要解决此问题,您可以通过使用“G17”标准数字格式string格式化Double值。 下面的示例使用带有Double值的“R”格式string,该值不能成功往返,还使用“G17”格式的string来成功往返原始值。

所以除非这个问题得到解决,否则你必须使用double.ToString(“G17”)进行往返。

更新 :现在有一个具体的问题来跟踪这个错误。

哇 – 一个3岁的问题,每个人似乎都错过了一个观点 – 即使是Jon Skeet! (@Jon:尊重,我希望自己不要愚弄自己。)

为了logging我运行的代码示例,并在我的环境 (Win10 64位AnyCPUdebugging,目标.NetFx 4.7)往返后的testing返回true。

这是一个实验。 数字alignment,以帮助指出…

这个代码…

  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; } } 

产生这个输出(星号***后来添加)…

  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 

这是人工完成的,但是在第一部分循环以小数点数0.0000000000000001的增量计数。
注意两个“连续值”(***)具有相同的内部二进制表示forms。

在第二部分 – 因为我们没有跳过强制十进制加法 – 内部价值保持在最低有效位滴答作响。 在5次迭代之后,两个10个值的序列不同步。

重点是(内部二进制)双打不能有精确的十进制表示,反之亦然。
我们只能尝试得到一个十进制string,代表我们的值“尽可能接近”。
这里,R格式化的string0.99999999999999745不明确地“最接近”0.9999999999999974或0.9999999999999975。

我很明白,这个问题似乎是“用相反的方式显示这个特性”(一个十进制表示模糊地映射到两个不同的二进制文件),但没有设法重新创build。
我们对双精度的限制毕竟是正确的,这就是为什么需要R格式的string。

我喜欢这样想“往返格式说明符会生成一个string, 表示可以往返的double值的最接近的double值 ”换句话说“R格式的string必须是往返行能够,不一定是价值。“

为了解决这个问题,人们不应该假定“价值 – >string – >相同的价值”是可能的,但是
应该可以依靠“value – > string – >附近的值 – >相同的string – >相同的附近值 – > …

记得

  1. 双打的内部performance取决于环境/平台

  2. 即使在完全微软的生态系统中,仍然有许多可能的变化

    一个。 构build选项(x86 / x64 / AnyCPU,发布/debugging)

    湾 硬件(英特尔CPU有80位寄存器用于算术 – debugging和发布构build代码可能会有所不同)

    C。 谁知道IL代码可能在哪里运行(在操作系统X / Y等64位下的32位模式)?

这应该“修复”原始问题的代码…

 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;