你好!昨天我试图写关于浮点数的文章,我发现自己对这个计算感到疑惑,使用 64 位浮点数:
>>> 0.1 + 0.2 0.30000000000000004
我意识到我并不完全理解它是如何工作的。我的意思是,我知道浮点计算是不精确的,但是:有一个浮点数比0.30000000000000004
更接近 0.3 !那么为什么我们得到答案0.30000000000000004
呢?
这并不总是发生——如您所料, 0.2 + 0.3
是0.5
。
如果您不想通过一堆非常详细的计算来阅读整篇文章,简短的回答是 0.1 + 0.2 恰好位于 2 个浮点数0.3
和0.30000000000000004
,答案是0.30000000000000004
,因为它的尾数是偶数。
浮点加法的工作原理
这大致是浮点加法的工作原理:
- 将数字加在一起(更精确)
- 将结果舍入到最接近的浮点数
所以让我们使用这些规则来计算 0.1 + 0.2。我昨天才知道浮点加法是如何工作的,所以我可能在这篇文章中犯了一些错误,但最后我确实得到了我预期的答案。
第 1 步:找出 0.1 和 0.2 是什么
首先,让我们使用 Python 计算出0.1
和0.2
的确切值是多少,作为 64 位浮点数。
>>> f"{0.1:.80f}" '0.10000000000000000555111512312578270211815834045410156250000000000000000000000000' >>> f"{0.2:.80f}" '0.20000000000000001110223024625156540423631668090820312500000000000000000000000000'
这些确实是精确的值:因为浮点数以 2 为底,所以您可以完全以 10 为底表示它们。有时您只需要很多数字 🙂
第 2 步:将数字相加
接下来,让我们将这些数字加在一起。我们可以将小数部分加在一起作为整数来得到准确的答案:
>>> 1000000000000000055511151231257827021181583404541015625 + 2000000000000000111022302462515654042363166809082031250 3000000000000000166533453693773481063544750213623046875
所以这两个浮点数的精确和是0.3000000000000000166533453693773481063544750213623046875
这不是我们的最终答案,因为0.3000000000000000166533453693773481063544750213623046875
不是 64 位浮点数。
第 3 步:查看最近的浮点数
现在,让我们看看0.3
附近的浮点数。这是最接近0.3
的浮点数(通常写成0.3
,尽管这不是它的确切值):
>>> f"{0.3:.80f}" '0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'
我们可以通过使用struct.pack
序列化0.3
到 8 个字节,加 1,然后使用struct.unpack
来找出0.3
之后的下一个浮点数:
>>> struct.pack("!d", 0.3) b'?\xd3333333' # manually add 1 to the last byte >>> next_float = struct.unpack("!d", b'?\xd3333334')[0] >>> next_float 0.30000000000000004 >>> f"{next_float:.80f}" '0.30000000000000004440892098500626161694526672363281250000000000000000000000000000'
所以0.3
左右的两个 64 位浮点数是0.299999999999999988897769753748434595763683319091796875
和0.3000000000000000444089209850062616169452667236328125
第 4 步:找出哪个最接近我们的结果
It turns out that 0.3000000000000000166533453693773481063544750213623046875
is exactly in the middle of 0.299999999999999988897769753748434595763683319091796875
and 0.3000000000000000444089209850062616169452667236328125
.
你可以通过这个计算看到:
>>> (3000000000000000444089209850062616169452667236328125000 + 2999999999999999888977697537484345957636833190917968750) // 2 == 3000000000000000166533453693773481063544750213623046875 True
所以他们两个都不是最接近的。
它怎么知道要舍入到哪一个?
在浮点数的二进制表示中,有一个数字称为“有效数”。在这种情况下(结果恰好在 2 个连续的浮点数之间,它将四舍五入到具有偶数尾数的那个。
在这种情况下是0.300000000000000044408920985006261616945266723632812500
实际上,我们之前已经看到了这个数字的有效数字:
- 0.30000000000000004 是
struct.unpack('!d', b'?\xd3333334')
- 0.3 是
struct.unpack('!d', b'?\xd3333333')
0.30000000000000004
的大端十六进制表示形式的最后一位是4
,所以这是偶数尾数(因为尾数是尾数)。
让我们也用二进制计算整个计算
上面我们用十进制进行了计算,因为这样读起来更直观。但当然,计算机不会以十进制进行这些计算——它们是以 2 为底的表示形式完成的。所以我也想知道它是如何工作的。
我不认为帖子的这个二进制计算部分特别清楚,但它对我写出来很有帮助。确实有很多数字,读起来可能很糟糕。
64 位浮点数的工作原理:指数和尾数
64 位浮点数用 2 个整数表示:指数和有效数以及 1 位符号。
这是指数和尾数如何对应于实际数字的等式
$$\text{sign} \times 2^\text{exponent} (1 + \frac{\text{significand}}{2^{52}})$$
例如,如果指数为1
,则尾数为2**51
且符号为正,我们将得到
$$2^{1} (1 + \frac{2^{51}}{2^{52}})$$
等于2 * (1 + 0.5)
或 3。
第 1 步:获取0.1
和0.2
的指数和尾数
我写了一些低效的函数来获取 Python 中正浮点数的指数和尾数:
def get_exponent(f): # get the first 12 bytes bytestring = struct.pack('!d', f) return int.from_bytes(bytestring, byteorder='big') >> 52 def get_significand(f): # get the last 52 bytes bytestring = struct.pack('!d', f) x = int.from_bytes(bytestring, byteorder='big') exponent = get_exponent(f) return x ^ (exponent << 52)
我忽略了符号位(第一位),因为我们只需要这些函数处理两个数字(0.1 和 0.2)并且这两个数字都是正数。
首先,让我们得到 0.1 的指数和尾数。我们需要减去 1023 才能得到实际的指数,因为浮点数就是这样工作的。
>>> get_exponent(0.1) - 1023 -4 >>> get_significand(0.1) 2702159776422298
这些数字一起工作得到0.1
的方式是2**exponent + significand / 2**(52 - exponent)
。
这是 Python 中的计算:
>>> 2**-4 + 2702159776422298 / 2**(52 + 4) 0.1
(您可能合理地担心此计算的浮点精度问题,但在这种情况下,我很确定它没问题,因为根据定义,这些数字没有精度问题——浮点数从2**-4
开始以1/2**(52 + 4)
) 的步长向上
我们可以为0.2
做同样的事情:
>>> get_exponent(0.2) - 1023 -3 >>> get_significand(0.2) 2702159776422298
下面是指数和尾数如何一起工作得到0.2
:
>>> 2**-3 + 2702159776422298 / 2**(52 + 3) 0.2
(顺便说一句,0.1 和 0.2 具有相同的尾数并非巧合——这是因为x
和2*x
始终具有相同的尾数)
第 2 步:重写0.1
以获得更大的指数
0.2
的指数大于0.1
– -3 而不是 -4。
所以我们需要重写
2**-4 + 2702159776422298 / 2**(52 + 4)
为X / (2**52 + 3)
如果我们在2**-4 + 2702159776422298 / 2**(52 + 4) = X / (2**52 + 3)
中求解 X,我们得到:
X = 2**51 + 2702159776422298 /2
我们可以很容易地在 Python 中计算它:
>>> 2**51 + 2702159776422298 //2 3602879701896397
第 3 步:添加有效数字
现在我们正在尝试做这个添加
2**-3 + 2702159776422298 / 2**(52 + 3) + 3602879701896397 / 2**(52 + 3)
所以我们需要把2702159776422298
和3602879701896397
>>> 2702159776422298 + 3602879701896397 6305039478318695
凉爽的。但是6305039478318695
大于 2**52 – 1(尾数的最大值),所以我们遇到了问题:
>>> 6305039478318695 > 2**52 True
第 4 步:增加指数
现在我们的答案是
2**-3 + 6305039478318695 / 2**(52 + 3)
首先,让我们减去 2**52 得到
2**-2 + 1801439850948199 / 2**(52 + 3)
这几乎是完美的,但是最后的2**(52 + 3)
需要一个2**(52 + 2)
。
所以我们需要将 1801439850948199 除以 2。这就是我们遇到错误的地方1801439850948199
是奇数!
>>> 1801439850948199 / 2 900719925474099.5
它恰好在两个整数之间,所以我们四舍五入到最接近的偶数(这是浮点规范所说的),所以我们最终的浮点数结果是:
>>> 2**-2 + 900719925474100 / 2**(52 + 2) 0.30000000000000004
这是我们期望的答案:
>>> 0.1 + 0.2 0.30000000000000004
这可能不是它在硬件中的工作方式
我在这里描述操作的方式并不完全是您进行浮点加法时发生的事情(例如,它不是“求解 X”),我相信有很多有效的技巧。但我认为这是同一个想法。
打印浮点数很奇怪
我们之前说过浮点数 0.3 不等于 0.3。实际上是这个数字:
>>> f"{0.3:.80f}" '0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'
那么,当您打印出该数字时,为什么它会显示0.3
?
计算机实际上并没有打印出数字的确切值,而是打印出最短的十进制数d
,它具有我们的浮点数f
是最接近d
的浮点数的属性。
事实证明,有效地做到这一点一点都不简单,并且有大量关于它的学术论文,例如快速准确地打印浮点数。或如何准确打印浮点数。
如果计算机打印出浮点数的确切值会不会更直观?
四舍五入到一个漂亮干净的十进制值很好,但在某种程度上我觉得如果计算机只是打印出浮点数的精确值可能会更直观——当你得到奇怪的结果时它可能看起来不那么令人惊讶.
To me, 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125 feels less surprising than 0.1 + 0.2 = 0.30000000000000004.
这可能是个坏主意,它肯定会占用大量屏幕空间。
就这样!
我有点怀疑是否有人有耐心遵循所有这些算术,但写下来对我很有帮助,所以我还是发表了这篇文章。希望其中一些是有道理的。
原文: https://jvns.ca/blog/2023/02/08/why-does-0-1-plus-0-2-equal-0-30000000000000004/