Jep*_*sen 15 .net c# floating-point rounding
在数学上,考虑这个问题的有理数
8725724278030350 / 2**48
Run Code Online (Sandbox Code Playgroud)
其中**在分母表示取幂,即,分母是2对48次方.(分数不在最低方面,通过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,然后你得到一个decimal是31准确!请参阅此Stack Overflow问题,以讨论将a转换Double为Decimal包含第一个舍入到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.
我们总结如下:
ToString()通过转换和轮15位数字Decimal在很多情况下不同意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定义为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,所描述的字符串转换似乎违反了该标准的二进制到十进制要求.
鉴于此,我很乐意将我的评估升级为"可能的错误".
首先看一下本页底部,它显示了一个非常相似的"双舍入"问题.
检查以下浮点数的二进制/十六进制表示形式表明给定范围以双重格式存储为相同的数字:
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位有效数字.