问题在于
IEEE 754浮点运算标准本质上是不精确的,几乎所有编程语言都会因此受到影响。
IEEE 754是一个非常复杂的话题,当你研究它几个月并且认为你完全明白时,其实你只是在自欺欺人!
准确的浮点数值比较是困难且容易出错的。在尝试比较浮点数之前,请仔细思考!
Excel程序通过在应用程序侧进行欺骗来解决这个问题。另一方面,VBA忠实地遵循IEEE 754规范
双精度(
binary64)。
双精度值使用64位内存表示。这64位被分成三个不同的字段,用于二进制科学计数法:
1.符号位(1位,表示值的正负)
2.指数(11位,由+1023偏置)
3.尾数(53位,52位存储+1位暗示)
在这个系统中,尾数利用了所有二进制数字以
1
开头的事实,因此
1
不存储在位模式中。它是暗示的,将尾数精度提高到53位。
数学计算如下:
存储值=符号值*2^未偏置指数*尾数
请注意,符号位的存储值为
1
表示负符号值(-1),而
0
表示正符号值(+1)。公式为
符号值=(-1)^(符号位)
。
问题总是归结为同一件事。
大多数
实数在这个系统中无法精确表示,这会引入小的舍入误差并像杂草一样传播。
将该系统视为一个间隔均匀的点网格可能有所帮助。该系统只能表示点值,不能表示点之间的任何实数。分配给浮点数的所有值都将舍入为点值之一(通常是最接近的点,但有些模式强制向上舍入到下一个最高点或向下舍入)。对浮点数值进行任何计算几乎肯定会导致结果值需要舍入。
显而易见,在这个网格中,相邻可表示点值之间有无限个实数;并且这些实数都被四舍五入到离散的网格点。
更糟糕的是,随着网格远离真正的零点(在两个方向上),每个二次幂处的间隙大小会加倍。例如,在范围为2到4的值之间的网格点之间的间隙长度是在范围为1到2的值之间的间隙长度的两倍。当表示具有足够大数量级的值时,网格间隙长度变得巨大,但靠近真正的零点时,它微不足道。
以下是您的示例数字...
以下二进制数表示 1.24
:
符号位 = 0
指数 = 01111111111
尾数 = 0011110101110000101000111101011100001010001111010111
完整的64位十六进制模式为:3FF3D70A3D70A3D7。
精度仅来自53位尾数,从二进制到精确的十进制值为:
0.2399999999999999911182158029987476766109466552734375
在这种情况下,由于与尾数相关联的隐藏位,暗示了前导整数1
,因此完整的十进制值为:
1.2399999999999999911182158029987476766109466552734375
现在请注意,这并不精确等于
1.24
,这就是整个问题所在。
让我们来看看1.42
:
符号位 = 0
阶码 = 01111111111
尾数 = 0110101110000101000111101011100001010001111010111000
完整的64位十六进制模式为:3FF6B851EB851EB8。
加上隐含的1
,完整的十进制数值被存储为:
1.4199999999999999289457264239899814128875732421875000
再次强调,不是精确的1.42
。
现在,让我们来看一下
1.6
:
符号位=0
指数=01111111111
尾数=1001100110011001100110011001100110011001100110011010
完整的64位十六进制模式为:3FF999999999999A。
请注意,在这种情况下,截断并
四舍五入了重复的二进制小数部分,当尾数位用完时?显然,以二进制base2表示的1.6永远不可能像以十进制base10表示的1/3那样精确(0.33333333333333333333333... ≠ 1/3)。
带有隐含的
1
,完整的十进制值存储为:
1.6000000000000000888178419700125232338905334472656250
不完全是 1.6
,但比其他值更接近!
现在让我们减去完整的存储双精度表示:
1.60
- 1.42
= 0.18000000000000015987
1.42
- 1.24
= 0.17999999999999993782
因此,您可以看到它们根本不相等。
解决这个问题的通常方法是threshold testing,基本上是检查两个值是否足够接近...这取决于您和您的要求。请注意,有效的阈值测试比表面上看起来要难得多。
这是一个函数,可以帮助您开始比较两个双精度数字。它可以很好地处理许多情况,但并非所有情况都可以。
Function Roughly(a#, b#, Optional within# = 0.00001) As Boolean
Dim d#, x#, y#, z#
Const TINY# = 1.17549435E-38 'SINGLE_MIN
If a = b Then Roughly = True: Exit Function
x = Abs(a): y = Abs(b): d = Abs(a - b)
If a <> 0# Then
If b <> 0# Then
z = x + y
If z > TINY Then
Roughly = d / z < within
Exit Function
End If
End If
End If
Roughly = d < within * TINY
End Function
这里的思路是,如果两个Double在一定范围内大致相同,则函数返回True
:
MsgBox Roughly(3.14159, 3.141591) '<---dispays True
Within margin默认值为0.00001,但您可以传递所需的任何余量。
虽然我们知道:
MsgBox 1.60 - 1.42 = 1.42 - 1.24 '<---dispays False
考虑一下它的实用性:
MsgBox Roughly(1.60 - 1.42, 1.42 - 1.24) '<---dispays True
@chris neilsen分享了一个有关Excel和IEEE 754的有趣Microsoft页面。
请阅读David Goldberg的开创性著作计算机科学家应该了解的浮点运算。它改变了我对浮点数的理解。