.NET的Double.ToString方法中的Round-two错误

在数学上,考虑这个问题的有理数

8725724278030350 / 2**48 

其中分母中的**表示取幂,即分母是248次幂。 (分数不是最低的,可以减少2.)这个数字可以完全表示为System.Double 。 它的十进制扩展是

 31.0000000000000'49'73799150320701301097869873046875 (exact) 

撇号不代表缺失的数字,而只是标记圆形到15的分数。 将执行17位数字。

请注意以下内容:如果此数字四舍五入为15位,则结果为31 (后跟十三个0秒),因为下一个数字( 49... )以4开头(意味着向下舍入)。 但如果该数字首先四舍五入为17位, 然后舍入为15位,则结果可能为31.0000000000001 。 这是因为第一轮舍入将49...数字增加到50 (terminates) (下一个数字为73... ),然后第二轮可能再次向上舍入(当中点舍入规则说“圆形”时远离零“)。

(当然,还有更多具有上述特征的数字。)

现在,事实certificate.NET的这个数字的标准字符串表示是"31.0000000000001"问题:这不是一个错误吗? 通过标准字符串表示,我们指的是由参数文件Double.ToString()实例方法生成的String ,它当然与ToString("G")生成的String相同。

一个有趣的事情是,如果你将上面的数字转换为System.Decimal那么你得到一个完全正确的31decimal ! 请参阅此Stack Overflow问题 ,以讨论将DoubleDecimal涉及首次舍入到15位数这一令人惊讶的事实。 这意味着转换为Decimal ToSting()正确的舍入值设置为15位数,而调用ToSting()则会生成不正确的舍入值。

总而言之,我们有一个浮点数,当输出给用户时,它是31.0000000000001 ,但是当转换为Decimal (其中29位可用)时,精确地变为31 。 这很不幸。

这里有一些C#代码供您validation问题:

 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 } 

上面的代码使用了Skeet的ToExactString方法。 如果你不想使用他的东西(可以通过URL找到),只需删除上面依赖于exactString的代码行。 你仍然可以看到Double有问题( evil )是如何舍入和转换的。

加成:

好的,所以我测试了一些更多的数字,这是一张表:

  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 

第一列给出Double表示的确切(虽然截断)值。 第二列给出了"R"格式字符串的字符串表示。 第三列给出了通常的字符串表示。 最后,第四列给出了转换此DoubleSystem.Decimal

我们总结如下:

  • ToString()舍入到15位数,在很多情况下通过转换为Decimal不同意舍入到15位数
  • 在许多情况下,转换为Decimal也会错误地舍入,并且这些情况中的错误不能被描述为“四次”错误
  • 在我的情况下, ToString()似乎产生比Decimal转换更大的数字,当他们不同意时(无论正确的两轮中的哪一轮)

我只是尝试过像上面这样的案例。 我没有检查是否存在其他“表单”数量的舍入错误。

因此,从您的实验中看来, Double.ToString似乎没有进行正确的舍入。

这是相当不幸的,但并不特别令人惊讶:对二进制到十进制转换进行正确的舍入是非常重要的,也可能非常慢,在极端情况下需要多精度算法。 请参阅David Gay的dtoa.c代码,以获取正确舍入的双字符串和字符串到双重转换所涉及的一个示例。 (Python目前使用此代码的变体进行float-to-string和string-to-float转换。)

即使是当前用于浮点运算的IEEE 754标准也建议使用 ,但不要求从二进制浮点类型到十进制字符串的转换始终是正确舍入的。 这是一个片段,来自第5.12.2节“表示有限数字的外部十进制字符序列”。

可能存在实现定义的有效位数限制,可通过正确的舍入到支持的二进制格式进行转换。 该限制H应该是H≥M+ 3,并且应该是H是无界的。

这里M被定义为所有支持的二进制格式bf的最大Pmin(bf) ,并且由于Pmin(float64)被定义为17并且.NET通过Double类型支持float64格式,因此在M应至少为17 。 简而言之,这意味着如果.NET遵循标准,它将提供正确的舍入字符串转换,最多至少20位有效数字。 所以看起来.NET Double不符合这个标准。

回答“这是一个错误”的问题,就像我希望它是一个错误一样,在数字格式化文档中我可以找到的任何地方似乎都没有任何准确性或IEEE 754一致性声明对于.NET。 所以它可能被认为是不受欢迎的,但我很难将其称为实际的错误。


编辑:Jeppe Stig Nielsen指出MSDN上的System.Double页面说明了这一点

Double符合IEC 60559:1989(IEEE 754)二进制浮点运算标准。

我不清楚这个合规声明究竟应涵盖哪些内容,但即使是1985年版本的IEEE 754,所描述的字符串转换似乎违反了该标准的二进制到十进制要求。

鉴于此,我很乐意将我的评估升级为“可能的错误”。

首先看一下本页底部,它显示了一个非常相似的“双舍入”问题。

检查以下浮点数的二进制/hex表示forms表明给定范围以双重格式存储为相同的数字:

 31.0000000000000480 = 0x403f00000000000e 31.0000000000000497 = 0x403f00000000000e 31.0000000000000515 = 0x403f00000000000e 

正如其他几个人所指出的,那是因为最接近的可表示的双精度值具有31.00000000000004973799150320701301097869873046875的精确值。

在IEEE 754向字符串的正向和反向转换中还需要考虑另外两个方面,特别是在.NET环境中。

首先(我找不到主要来源)来自维基百科:

如果具有最多15个有效小数的十进制字符串转换为IEEE 754双精度,然后转换回相同的有效小数,则最终字符串应与原始字符串匹配; 如果将IEEE 754双精度转换为具有至少17个有效小数的十进制字符串,然后再转换回双精度,则最终数字必须与原始数字匹配。

因此,关于符合标准,将字符串31.0000000000000497转换为double在转换回字符串时不一定相同(给出的小数位数太多)。

第二个考虑因素是,除非double到string转换有17位有效数字,否则它的舍入行为也没有在标准中明确定义。

此外, Double.ToString()上的文档显示它由当前区域性设置的数字格式说明符控制。

可能的完整说明:

我怀疑两次舍入发生的情况如下:初始十进制字符串创建为16或17位有效数字,因为这是“往返”转换所需的精度,给出中间结果31.00000000000005或31.000000000000050。 然后由于默认文化设置,结果四舍五入到15位有效数字31.00000000000001,因为15位十进制有效数字是所有双精度数的最小精度。

另一方面,执行到Decimal的中间转换,以不同的方式避免此问题:它直接截断为15位有效数字 。

问题:这不是一个错误吗?

是。 在GitHub上看到这个PR 。 两次AFAK舍入的原因是“漂亮”格式输出,但它引入了一个你已经在这里发现的错误。 我们试图修复它 – 删除15位精确转换,直接转到17位精确转换。 坏消息是它是一个突破性的变化,会破坏很多东西。 例如,其中一个测试用例将中断:

10:12:26 Assert.Equal() Failure 10:12:26 Expected: 1.1 10:12:26 Actual: 1.1000000000000001

该修复程序将影响大量现有库,因此最终此PR已暂停。 但是,.NET Core团队仍在寻找修复此错误的机会。 欢迎加入讨论。

我有一个更简单的怀疑:罪魁祸首很可能是pow operator => **; 虽然您的数字可以完全表示为双精度数,但是出于方便的原因(功率运算符需要大量工作才能正常工作),功率由指数函数计算。 这是您可以通过重复使用数字而不是使用pow()来优化性能的一个原因,因为pow()非常昂贵。

所以它没有给你正确的2 ^ 48,但有点不正确,因此你有你的舍入问题。 请查看2 ^ 48确切返回的内容。

编辑:对不起,我只对这个问题进行了扫描并给出了错误的怀疑。 英特尔处理器上存在双舍入的已知问题。 较旧的代码使用FPU的内部80位格式而不是可能导致错误的SSE指令。 该值完全写入80位寄存器,然后舍入两次 ,因此Jeppe已经找到并巧妙地解释了这个问题。

这是一个错误吗? 好吧,处理器正在做正确的事情,这只是英特尔FPU内部具有更高精度的浮点运算的问题。

进一步编辑和信息:“双舍入”是一个已知问题,并在Jean-Michel Muller等人的“浮点运算手册”中有明确提及。 人。 在第3.3页“3.3.1典型问题:’双舍入’”下的“需要修订”一章中:

正在使用的处理器可能提供比程序变量精度更宽的内部精度(典型示例是英特尔平台上可用的双扩展格式,当程序的变量是单精度或双精度时浮点数字)。 这可能有些奇怪的副作用,我们将在本节中看到。 考虑C程序[…]

 #include  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位:Linux / Debian上的GCC 4.1.2 20061115

使用Compilerswitch或-mfpmath = 387(80bit-FPU):3.6893488147419103232e + 19 -march = pentium4 -mfpmath = sse(SSE)oder 64-bit:3.6893488147419111424e + 19

正如书中所解释的,差异的解决方案是80位和53位的双舍入。