【问题标题】:Why is `9.3 == 9.3.to_d` false?为什么`9.3 == 9.3.to_d` 是假的?
【发布时间】:2018-08-07 10:41:37
【问题描述】:

我刚刚在 TDD 期间遇到了一个有趣的案例:

 Failure/Error: expect(MoneyManager::CustomsCalculator.call(price: 31,    weight: 1.12)).to    eq 9.3

   expected: 9.3
        got: 0.93e1

我进一步调查发现:

require 'bigdecimal'
 => true
2.4.2 :005 > require 'bigdecimal/util'
 => true
...
2.4.2 :008 > 1 == 1.to_d
 => true
2.4.2 :009 > 2 == 2.to_d
 => true
2.4.2 :010 > 2.0 == 2.0.to_d
 => true
2.4.2 :011 > 1.3 == 1.3.to_d
 => true
2.4.2 :012 > 9.3 == 9.3.to_d
 => false

为什么是9.3 == 9.3.to_d false

PS,我很清楚 Float 和 BigDecimal 是什么,但我对这种特殊行为感到非常困惑。

【问题讨论】:

  • 你不应该对浮点值进行相等性测试。您总是需要处理浮点数不准确的内部表示问题,因此 == 和 != 并不是非常有用。如果您检查差异1.3.to_d - 1.3,您会发现它不完全是 0
  • 如修补程序所述。 (f1 - f2).abs < EPSILON 是比较浮点数的传统方式。使用平等只会让人流泪。这种方法对运行代码的机器也更好。
  • 这种行为也不仅仅适用于bigdecimal。我与 Ruby 和打包字符串进行了很多互操作。 [0.123].pack('f').unpack('f') #=> 0.12300000339746475 使用 == 是毫无价值的。但如果使用精度为 7 位 0.0000001 的 epsilon 值,则结果符合预期。
  • 9.3 == 9.3.to_d.to_f #=> true ????
  • 也许这是关于 BigDecimal 的默认精度 Float::DIG(即 15)的一个错误:9.3 == 9.3.to_d(15) #=> false9.3 == 9.3.to_d(16) #=> true

标签: ruby floating-point decimal precision


【解决方案1】:

这并不是真正的“红宝石问题”。这是一个数字的浮点表示问题。

您不能可靠地在浮点数和“精确”值(由BigDecimal 表示)之间执行相等检查。

BigDecimal.new(9.3, 2) 是准确的。 9.3 不是。

9.3 * 100 #=> 930.0000000000001
1.3 * 100 #=> 130.0

这就是二进制浮点数的工作原理。它们(有时)是“真实”值的不精确表示。

您可以:

  • 比较同类(bigdecimal1 == bigdecimal2,或float1 == float2)。但还要注意,如果您执行不同的计算来获得这些值,那么比较 float1 == float2 也是不可靠的!或者,
  • 检查值是否相等在错误范围内(例如,在rspec 术语中,expect(value1).to be_within(1e-12).of(value2))。

【讨论】:

  • 这表明了一个不正确的浮点模型。 “他们不准确”的说法是错误的。 (它们也不限于有限或二进制。)如 IEEE 754 中规定的,浮点数精确地表示一个数字。相反,操作被定义为返回四舍五入到最接近的可表示值的数学结果。浮点数是精确的,但运算可能不精确。理解这种区别对于正确推理浮点计算和设计计算至关重要。
  • 具有讽刺意味的是,比较浮点值是否相等总是准确的,但最常见(并且错误地)被认为是不可靠的。当且仅当比较的两个项目完全相等时,比较相等性返回 true。如果它在用户希望它返回 true 时返回 false,那是因为之前有一个错误,而不是比较中的错误。这说明了为什么指责比较相等或说浮点数不准确是误导性的:当问题实际上在别处时,它们错误地引导人们在比较中寻找问题。
  • 当我说“它们不精确”时,我的意思是“它们(有时)是一种不精确的方式来表示您真正想要表达的价值”。例如,9.3 的浮点表示实际上并不意味着9.3。 (它是9.300000000000001。)我的回答也确实表明比较float1 == float2 是有效的,但这是一个冒险的策略,因为在某些情况下你可能会得到一个微妙/几乎无法追踪的错误。
  • @EricPostpischil representation 已经不准确。我们看到的不是实际值(9.300000000000000710542735760100185871124267578125),而是近似值(9.3)。
  • @Stefan:9.300000000000000710542735760100185871124267578125 被格式化为显示(默认设置)为“9.3”这一事实并不意味着它代表 9.3。正如我所写,IEEE 754 对此很清楚;一个浮点对象设置为 9.300000000000000710542735760100185871124267578125 代表 9.300000000000000710542735760100185871124267578125 完全不代表 9.3。当对其进行算术运算时,它会表现得好像是 9.300000000000000710542735760100185871124267578125,一般不会表现得好像是 9.3。
【解决方案2】:

因上述 Eric 评论而编辑

您可以使用 float 的性质并将其与您建议的限制进行比较,该限制将可靠地返回 truefalse

(bigdecimal-float).abs < comparison_limit

在您的示例中(我已添加 () 以提高可读性):

((9.3.to_d)-9.3).abs < 0.000001  <-- watch out for the limit!

产生true,可用于测试。

编辑基于 Eric 的(谢谢)评论。 在比较两个数字时,务必检查公差范围。

你可以这样做:

9.3.next_float

这会给你

9.300000000000002

所以你的容忍度应该是

0.000000000000002

注意:注意步骤:

9.3.next_float.next_float
=> 9.300000000000004

现在代码看起来不同了:

((9.3.to_d)-9.3).abs < 0.000000000000002

【讨论】:

  • 与容差比较会减少假阴性,但会增加假阳性。所以像这样的代码可能会给软件带来新的错误。
  • @EricPostpischil 在一般情况下你是对的。如果我引用您的上述评论“操作被定义为返回四舍五入到最接近的可表示值的数学结果”。如果您使用设置的容差作为最大可表示值,那么您的代码不需要增加误报(没有引入新的错误)。
  • IEEE-754 浮点的最大可表示值是无穷大。将容差设置为无穷大当然会在程序中产生巨大的错误。我不确定您可能指的是什么其他价值。浮点计算序列中的错误可能会复合或消除,这取决于算法的性质和特定数据。因此,对于设置什么容差没有一般规则。一些算法自然收敛,容差可以设置为 1 ULP。一些算法存在分歧,预期的错误可能是无限的。
  • 在任何情况下,在任何正常情况下,与容差进行比较都会承认假阴性。这是不可避免的:如果代码被修改为接受相同的数字,如果使用精确数学计算将相等,但由于以有限的精度计算而不同,那么代码也将接受如果使用计算将不相等的相等数字精确的数学,但很接近。任何人都不应接受与公差进行比较的建议,并将其放入他们的代码中而不评估其造成的不利影响。
  • @EricPostpischil“评估它造成的不利影响” - 你是对的,应该考虑添加任何代码。由于计算机(硬件、cpu 等)本质上是有限的机器,因此它们的精度也有限。每个平台(Intel、Risc/V 等)都有不同的精度,在编写此类代码时一定要考虑到这一点。
猜你喜欢
  • 2016-07-27
  • 2018-12-28
  • 1970-01-01
  • 2022-07-04
  • 1970-01-01
  • 2014-08-12
  • 2015-07-20
  • 1970-01-01
  • 2015-03-25
相关资源
最近更新 更多