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

Jep*_*sen 15 .net c# floating-point rounding

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

8725724278030350 / 2**48
Run Code Online (Sandbox Code Playgroud)

其中**在分母表示取幂,即,分母是248次方.(分数不在最低方面,通过2可还原的)此数目是恰好作为表示的System.Double.它的十进制扩展是

31.0000000000000'49'73799150320701301097869873046875 (exact)
Run Code Online (Sandbox Code Playgroud)

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

请注意以下内容:如果此数字四舍五入为15位,则结果为31(后跟十三0秒),因为下一个数字(49...)以a开头4(意味着向下舍入).但如果数字首先四舍五入为17位,然后四舍五入为15位,结果可能是31.0000000000001.这是因为第一次舍入通过将49...数字增加到50 (terminates)(下一个数字73...)而向上舍入,然后第二次舍入可能再次向上舍入(当中点舍入规则表示"从零开始舍入"时).

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

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

需要注意的一个有趣的事情是,如果你投的上述号码System.Decimal,然后你得到一个decimal31准确!请参阅此Stack Overflow问题,以讨论将a转换DoubleDecimal包含第一个舍入到15位的令人惊讶的事实.这意味着转换为Decimal正确的舍入到15位数,而调用ToSting()是不正确的.

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

上面的代码使用了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
Run Code Online (Sandbox Code Playgroud)

第一列给出了表示的精确(尽管截断)值Double.第二列给出了"R"格式字符串的字符串表示.第三列给出了通常的字符串表示.最后,第四列给出了System.Decimal转换它的结果Double.

我们总结如下:

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

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

Mar*_*son 7

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

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

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

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

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

在回答"这是一个错误"的问题,因为我更喜欢它是一个错误,有确实似乎没有准确性或IEEE的任何索赔754一致性的任何地方,我可以数字格式文档中找到对于.NET.所以它可能被认为是不受欢迎的,但我很难将其称为实际的错误.


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

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

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

鉴于此,我很乐意将我的评估升级为"可能的错误".

  • @Jeppe Stig Nielsen:刚刚找到1985版的副本; 即使在那里,如果我正确读取它,它们指定一个范围,在该范围内二进制到十进制(即字符串)转换应该正确舍入,并且您的示例落在该范围内. (4认同)

nic*_*las 6

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

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

31.0000000000000480 = 0x403f00000000000e
31.0000000000000497 = 0x403f00000000000e
31.0000000000000515 = 0x403f00000000000e
Run Code Online (Sandbox Code Playgroud)

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

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

我们有来自维基百科的第一个(我找不到主要来源):

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

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

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

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

可能的完整说明:

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

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

  • @nicholas:绝大多数主导的IEEE 754浮点标准非常基于单个浮点数表示精确值而不是范围的模型:操作的"正确"结果是与浮点数最接近的浮点数.用那些确切的输入值计算的数学运算.在这种情况下,*exact*值是OP给出的值,并且通过正确的舍入,舍入到15位数应该给出"31.0",而不是"31.0 ... 01".不幸的是,似乎`ToString`无法在这里进行正确的舍入. (3认同)
  • 好的,所以`0x403f00000000000e`(你的符号)内部给出的数字可以说代表一个整个区间(长度为'2**-48`,你会同意的).您的示例证明此间隔包含属于"31"的数字和属于"31.0000000000001"的数字.所以**在你看来**它应该是"未定义的行为"这两个字符串`ToString()`中的哪一个选择?在我看来**它应该使用该间隔的中点,而中点属于"31". (2认同)