【问题标题】:Are C doubles different to .NET doubles?C双打与.NET双打不同吗?
【发布时间】:2012-06-05 12:09:21
【问题描述】:

比较一些 C 代码和我试图替换它的 F#,我观察到最终结果存在一些差异。

在备份代码时,我发现即使存在差异 - 尽管很小。

代码首先从文件中读取数据。第一个数字的结果不同。例如,在 F# 中(更易于编写脚本):

let a = 71.9497985840
printfn "%.20f" a

我得到了预期的(对我而言)输出71.94979858400000000000

但在 C 中:

a =  71.9497985840;
fprintf (stderr, "%.20f\n", a);

打印出71.94979858400000700000

那个 7 是从哪里来的?

差异很小,但它困扰着我,因为我不知道为什么。 (这也让我很困扰,因为它让我更难追踪我的两个版本代码的分歧)

【问题讨论】:

  • 你确定两个值都是双倍的吗?您确定错误不在打印中吗?
  • .net 中的浮点运算没有明确定义的精确结果。这只是一个近似值。编译器和 JITer 会随心所欲地更改低位数字,只需用 printfn 之类的东西观察它们就会影响它们。
  • 使用调试器或其他工具查看变量的确切位模式。如果您拥有的 F# 和 C 实现都使用相同的浮点格式并且变量具有完全相同的位模式,那么区别在于打印或编译器如何解析浮点文字。在任何一种情况下,由于浮点数只是近似值,因此可能没有什么可担心的,因为差异非常小。
  • 我看到至少三种不同的可能性:1)ToString 转换不同 2)编译器中的 String 到 Double 转换不同 3).net 代码使用了更高的精度数(通常很常见,但非常在这种特定情况下不太可能)
  • 您在 F# 中看到的值不是确切的值。在此处尝试 DoubleConverter 类以查看确切值:stackoverflow.com/a/3495263/93652

标签: .net c floating-point


【解决方案1】:

这是印刷的区别。将该值转换为 IEEE754 double 产生

Prelude Text.FShow.RealFloat> FD 71.9497985840
71.94979858400000694018672220408916473388671875

71.949798584 的表示足以将数字与其邻居区分开来。 C,当被要求以小数点后 20 位的精度打印时,将值正确舍入为所需的位数,显然 F# 使用最短的唯一确定表示并用所需的 0 数填充它,就像 Haskell 一样.

【讨论】:

  • 那么,如果我对这些数字的列表执行多个操作,这些差异会开始变得显着吗?我正在尝试弄清楚如何测试以查看是否是这种情况...
  • 这取决于。我认为 .NET 为 JITter 提供了相当大的余地,因此结果不一定可以在 .NET 代码的不同运行中重现。如果 C 实现不声称遵循 IEC 60559,它也有很大的余地。问题是 C 和 .NET 对于具有相同输入的相同函数是否会产生相同的结果。如果他们这样做,您将只能观察到字符串表示的差异,它只出现在确定double 值所需的部分后面,没有任何东西可以累积。如果他们在结果中没有 1 或 2 ULP 的差异......
  • 可以累积。但除非您进行 大量 操作(或一些条件不佳的操作),否则差异将很小。
  • 谢谢,我现在并排运行它们,每一步都慢慢注意到它们的分歧有多远......我们会看到。
  • @Benjol:澄清 Daniel Fischer 所说的:只要 .NET 和 C 运行时都遵循 IEEE-754 标准(也称为 ISO/IEC 60559),那么计算的实际值将是相同的。您在此处观察到的不同之处在于运行时 print 的值不同,而不是表示的值存在实际差异。所以任何额外的操作都不会导致这个错误累积,因为“错误”(或者更严格地说,差异)只有在将结果转换为十进制以便显示时才会出现。
【解决方案2】:

这只是不同的四舍五入。数字是相同的(至少根据 CPython):

>>> '%.44f' % 71.94979858400000000000
'71.94979858400000694018672220408916473388671875'
>>> '%.44f' % 71.94979858400000700000
'71.94979858400000694018672220408916473388671875'

【讨论】:

    【解决方案3】:

    区别在于 .NET System.Double.ToString() 方法,该方法将双精度数转换为字符串。您可以通过下载 SSCLI20 中提供的 CLR 源代码来查看相关代码。转换由 clr/src/vm/comnumber.cpp, COMNumber::FormatDouble() 函数完成。看起来like this,代码中的注释最能说明发生了什么:

    //In order to give numbers that are both friendly to display and round-trippable,
    //we parse the number using 15 digits and then determine if it round trips to the same
    //value.  If it does, we convert that NUMBER to a string, otherwise we reparse using 17 digits
    //and display that.
    

    C 运行时库没有该功能。

    【讨论】:

    • 谢谢汉斯,这是一个相当确凿的答案。当我继续我的算法时,我肯定会遇到累积错误,现在我只需要尝试准确追踪在哪里以及为什么......目前无法解决它是这个浮点数的东西,还是我的简单错误代码。
    【解决方案4】:

    其他答案充分解释了问题的根源(双精度和舍入)。

    如果您的数字通常大小适中,并且小数精度非常重要(比计算速度更重要),那么也许可以考虑使用 .NET decimal 格式。这为您提供了 28-29 个精确的小数位精度,而没有像 double 那样的小数二进制舍入错误。限制是范围更小(没有大指数!)。

    http://msdn.microsoft.com/en-us/library/364x0z75%28v=vs.100%29.aspx

    【讨论】:

      【解决方案5】:

      为遇到此问题的人提供更多信息。

      使用找到的代码位 here,我已经确认(我相信)底层二进制表示(至少对于这个特定数字)是相同的断言。

      这里是代码示例 - 请注意“将零乘以零”以消除负零 - 当转换为 long 时这很难看。

      //(C# this time)
      var d = 71.9497985840;   //or other incoming double value
      if(d == 0) d = d * d;    //for negative zero
      var longval = System.BitConverter.DoubleToInt64Bits(d); // = 4634763433907061836
      

      在 C 中:

      double d;
      long long a;
      d = 71.9497985840;      //or other incoming double value
      if(d == 0) d = d * d;   //for negative zero
      a = *(long long*)&d;    //= 4634763433907061836
      

      更新 - 我跟进,发现在矩阵求逆过程中引入了差异,因为每个系统调用不同的库,以不同的方式实现求逆...

      【讨论】:

        猜你喜欢
        • 2012-02-12
        • 1970-01-01
        • 2023-03-10
        • 2020-06-14
        • 1970-01-01
        • 2016-10-05
        • 2014-01-17
        • 1970-01-01
        • 2011-01-01
        相关资源
        最近更新 更多