15

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

8725724278030350 / 2**48

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

31.0000000000000'49'73799150320701301097869873046875 (exact)

其中撇号不代表缺失的数字,而只是标记四舍五入到15的边界。将执行17位数字。

请注意以下几点:如果此数字四舍五入为 15 位,则结果将为31(后跟 130个 s),因为接下来的数字 ( 49...) 以 a 开头(表示向下4舍入)。但如果先将数字四舍五入为 17 位,然后再四舍五入为 15 位,则结果可能为. 这是因为第一个舍入通过将数字增加到(下一个数字是)来进行舍入,然后第二个舍入可能会再次舍入(当中点舍入规则说“从零舍入”时)。31.000000000000149...50 (terminates)73...

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

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

需要注意的一件有趣的事情是,如果您将上述数字转换为,System.Decimal那么您将得到一个decimal完全正确的数字31!请参阅此 Stack Overflow 问题Double,了解关于将 a 转换为Decimal涉及第一次四舍五入到 15 位这一令人惊讶的事实的讨论。这意味着强制转换Decimal为 15 位数字是正确的,而调用ToSting()是错误的。

总而言之,我们有一个浮点数,当输出给用户时,它是31.0000000000001,但当转换为Decimal29位可用)时,它变成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
}

上面的代码使用了 Skeet 的ToExactString方法。如果你不想使用他的东西(可以通过网址找到),只需删除上面依赖的代码行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"格式字符串的字符串表示。第三列给出了通常的字符串表示。最后第四列给出了System.Decimal转换 this 的结果Double

我们得出以下结论:

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

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

4

5 回答 5

9

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

这是相当不幸的,但并不特别令人惊讶:对二进制到十进制的转换进行正确的舍入是不平凡的,而且可能非常慢,在极端情况下需要多精度算术。请参阅此处的David Gay 的dtoa.c代码,了解正确舍入的 double 到字符串和字符串到 double 转换所涉及的一个示例。(Python 当前使用此代码的变体进行浮点到字符串和字符串到浮点的转换。)

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

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

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

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


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

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

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

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

于 2012-06-18T16:36:22.017 回答
6

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

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

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

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

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

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

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

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

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

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

可能的完整解释:

我怀疑两次舍入正在发生这样的事情:初始十进制字符串创建为 16 或 17 位有效数字,因为这是“往返”转换所需的精度,中间结果为 31.00000000000005 或 31.000000000000050。然后由于默认的区域性设置,结果被舍入到 15 个有效数字,31.00000000000001,因为 15 个十进制有效数字是所有双精度的最小精度。

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

于 2012-06-18T15:11:55.967 回答
1

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

是的。在 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 团队仍在寻找修复此错误的机会。欢迎加入讨论。

于 2017-09-11T09:08:04.787 回答
0

截断是限制稍后将被舍入的数字精度的正确方法,正是为了避免双重舍入问题。

于 2020-11-12T10:35:28.040 回答
-1

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

所以它没有给你正确的 2^48,但有些不正确,因此你有四舍五入的问题。请查看 2^48 究竟返回了什么。

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

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

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

所使用的处理器可能提供比程序变量的精度更宽的内部精度(一个典型的例子是英特尔平台上可用的双扩展格式,当程序的变量是单精度或双精度时浮点数字)。正如我们将在本节中看到的,这有时可能会产生奇怪的副作用。考虑 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) 或 64 位:3.6893488147419111424e+19

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

于 2012-06-18T18:55:48.340 回答