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

时间:2012-06-18 14:33:11

标签: c# .net floating-point rounding

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

8725724278030350 / 2**48

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

31.0000000000000'49'73799150320701301097869873046875 (exact)

其中撇号不代表缺失的数字,而只是标记在哪里舍入到 15 的boudary。 17 数字将被执行。

请注意以下几点:如果此数字四舍五入为15位,则结果为31(后跟十三0 s),因为下一个数字(49...)以a开头4(意思是围绕向下)。但如果数字 first 舍入为17位且舍入为15位,结果可能为31.0000000000001。这是因为第一轮舍入会将49...数字增加到50 (terminates)(下一个数字为73...),然后第二轮可能再次向上舍入(当中点舍入规则时)说“离开零点”)。

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

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

有一点值得注意的是,如果你将上面的数字转换为System.Decimal,那么你会得到decimal 31!有关将Double转换为Decimal涉及首次舍入为15位的令人惊讶的事实,请参阅this Stack Overflow question。这意味着转换为Decimal会将正确的舍入值设为15位,而调用ToSting()会使得31.0000000000001不正确。

总而言之,我们有一个浮点数,在输出给用户时为Decimal,但转换为31时(其中 29 位数为可用),完全成为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 } 。这很不幸。

以下是一些用于验证问题的C#代码:

ToExactString

上面的代码使用了Skeet的exactString方法。如果您不想使用他的东西(可以通过URL找到),只需删除上面依赖于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"表示的确切(虽然截断)值。第二列给出了System.Decimal格式字符串的字符串表示。第三列给出了通常的字符串表示。最后,第四列提供了转换此Double后产生的ToString()

我们总结如下:

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

我只试验过上述情况。我没有检查是否存在其他“表单”数量的舍入错误。

5 个答案:

答案 0 :(得分:7)

因此,根据您的实验,Double.ToString似乎没有进行正确的舍入。

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

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

  

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

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

在回答'这是一个错误'的问题时,就像我喜欢它是一个错误一样,似乎没有任何关于准确性或IEEE 754一致性的要求我可以在.NET的数字格式文档中找到的任何地方。所以它可能被认为是不合需要的,但我很难将其称为实际的错误。


编辑:Jeppe Stig Nielsen指出MSDN上的System.Double页面指出

  

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

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

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

答案 1 :(得分:6)

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

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

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

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

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

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

  

如果转换了最多15个有效小数的十进制字符串   以IEEE 754双精度然后转换回相同   有效小数的数量,那么最终的字符串应该匹配   原版的;如果IEEE 754双精度转换为a   十进制字符串,至少有17位有效小数,然后转换   返回加倍,然后最终的数字必须与原始数字匹配。

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

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

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

可能的完整说明:

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

另一方面,将中间转换为Decimal,可以另外一种方式避免此问题:直接truncates to 15 significant digits

答案 2 :(得分:1)

  

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

是。见this PR on GitHub。二次AFAK四舍五入的原因是"漂亮"格式输出,但它引入了一个你已经在这里发现的bug。我们试图修复它 - 删除15位精度转换,直接转到17位精度转换。坏消息是它是一个突破性的变化,会破坏很多东西。例如,其中一个测试用例将会中断:

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

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

答案 3 :(得分:0)

我有一个更简单的怀疑:罪魁祸首很可能是pow operator => **; 虽然您的号码 完全可以表示为双倍,但为方便起见 (电力运营商需要工作才能正常工作)电力计算 通过指数函数。这是您可以优化性能的一个原因 通过重复乘以一个数而不是使用pow(),因为pow()是非常的 昂贵。

<击>

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

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

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

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

  

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

#include <stdio.h>

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位的双舍入。

答案 4 :(得分:0)

截断是限制以后要取整的数字精度的正确方法,正是为了避免出现双取整问题。