0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

浮点加法是如何计算的

dyquk4xk2p3d 来源:良许Linux 2023-05-26 15:26 次阅读

嗨!我试着写点关于浮点数的东西,我发现自己对这个 64 位浮点数的计算方法很好奇:


>>> 0.1 + 0.2

0.30000000000000004

我意识到我并没有完全理解它是如何计算的。我的意思是,我知道浮点计算是不精确的,你不能精确地用二进制表示0.1,但是:肯定有一个浮点数比0.30000000000000004更接近 0.3!那为什么答案是0.30000000000000004呢?

如果你不想阅读一大堆计算过程,那么简短的答案是:0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125正好位于两个浮点数之间,即0.299999999999999988897769753748434595763683319091796875(通常打印为0.3) 和0.3000000000000000444089209850062616169452667236328125(通常打印为0.30000000000000004)。答案是0.30000000000000004,因为它的尾数是偶数。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

浮点加法是如何计算的

以下是浮点加法的简要计算原理:

◈把它们精确的数字加在一起 ◈将结果四舍五入到最接近的浮点数

让我们用这些规则来计算 0.1 + 0.2。我昨天才刚了解浮点加法的计算原理,所以在这篇文章中我可能犯了一些错误,但最终我得到了期望的答案。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

第一步:0.1 和 0.2 到底是多少

首先,让我们用 Python 计算0.1和0.2的 64 位浮点值。


>>> f"{0.1:.80f}"

'0.10000000000000000555111512312578270211815834045410156250000000000000000000000000'

>>> f"{0.2:.80f}"

'0.20000000000000001110223024625156540423631668090820312500000000000000000000000000'

这确实很精确:因为浮点数是二进制的,你也可以使用十进制来精确的表示。但有时你只是需要一大堆数字:)

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

第二步:相加

接下来,把它们加起来。我们可以将小数部分作为整数加起来得到确切的答案:


>>> 1000000000000000055511151231257827021181583404541015625 + 2000000000000000111022302462515654042363166809082031250

3000000000000000166533453693773481063544750213623046875

所以这两个浮点数的和是0.3000000000000000166533453693773481063544750213623046875。

但这并不是最终答案,因为它不是一个 64 位浮点数。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

第三步:查找最接近的浮点数

现在,让我们看看接近0.3的浮点数。下面是最接近0.3的浮点数(它通常写为0.3,尽管它不是确切值):


>>> f"{0.3:.80f}"

'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

我们可以通过struct.pack将0.3序列化为 8 字节来计算出它之后的下一个浮点数,加上 1,然后使用struct.unpack:


>>> struct.pack("!d", 0.3)

b'?xd3333333'

# 手动加 1

>>> next_float = struct.unpack("!d", b'?xd3333334')[0]

>>> next_float

0.30000000000000004

>>> f"{next_float:.80f}"

'0.30000000000000004440892098500626161694526672363281250000000000000000000000000000'

当然,你也可以用math.nextafter:


>>> math.nextafter(0.3, math.inf)

0.30000000000000004

所以0.3附近的两个 64 位浮点数是0.299999999999999988897769753748434595763683319091796875和0.3000000000000000444089209850062616169452667236328125。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

第四步:找出哪一个最接近

结果证明0.3000000000000000166533453693773481063544750213623046875正好在0.299999999999999988897769753748434595763683319091796875和0.3000000000000000444089209850062616169452667236328125的中间。

你可以通过以下计算看到:


>>> (3000000000000000444089209850062616169452667236328125000 + 2999999999999999888977697537484345957636833190917968750) // 2 == 3000000000000000166533453693773481063544750213623046875

True

所以它们都不是最接近的。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

如何知道四舍五入到哪一个?

在浮点数的二进制表示中,有一个数字称为“尾数”。这种情况下(结果正好在两个连续的浮点数之间),它将四舍五入到偶数尾数的那个。

在本例中为0.300000000000000044408920985006261616945266723632812500。

我们之前就见到了这个数字的尾数:

◈0.30000000000000004 是struct.unpack('!d', b'?xd3333334')的结果 ◈0.3 是struct.unpack('!d', b'?xd3333333')的结果

0.30000000000000004的大端十六进制表示的最后一位数字是4,它的尾数是偶数(因为尾数在末尾)。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

我们用二进制来算一下

之前我们都是使用十进制来计算的,这样读起来更直观。但是计算机并不会使用十进制,而是用 2 进制,所以我想知道它是如何计算的。

我不认为本文的二进制计算部分特别清晰,但它写出来对我很有帮助。有很多数字,读起来可能很糟糕。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

64 位浮点数如何计算:指数和尾数

64 位浮点数由 2 部分整数构成:指数和尾数,还有 1 比特符号位.

以下是指数和尾数对应于实际数字的方程:

7902e3d2-fae3-11ed-90ce-dac502259ad0.jpg

例如,如果指数是1,尾数是2**51,符号位是正的,那么就可以得到:

7908885a-fae3-11ed-90ce-dac502259ad0.jpg

它等于2 * (1 + 0.5),即 3。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

步骤 1:获取 0.1 和 0.2 的指数和尾数

我用 Python 编写了一些低效的函数来获取正浮点数的指数和尾数:


def get_exponent(f):

# 获取前 52 个字节

bytestring = struct.pack('!d', f)

return int.from_bytes(bytestring, byteorder='big') >> 52

def get_significand(f):

# 获取后 52 个字节

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

它们根据2**指数 + 尾数 / 2**(52 - 指数)这个公式得到0.1。

下面是 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总是有相同的尾数。)

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

步骤 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 = 2**51 + 2702159776422298 /2

在 Python 中,我们很容易得到:


>>> 2**51 + 2702159776422298 //2

3602879701896397

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

步骤 3:添加符号位

现在我们试着做加法:


2**-3 + 2702159776422298 / 2**(52 + 3) + 3602879701896397 / 2**(52 + 3)

我们需要将2702159776422298和3602879701896397相加:


>>> 2702159776422298 + 3602879701896397

6305039478318695

棒。但是6305039478318695比2**52-1(尾数的最大值)大,问题来了:


>>> 6305039478318695 > 2**52

True

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

第四步:增加指数

目前结果是:


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

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

硬件中它可能并不是这样工作的

在硬件中做浮点数加法,以上操作方式可能并不完全一模一样(例如,它并不是求解 “X”),我相信有很多有效的技巧,但我认为思想是类似的。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

打印浮点数是非常奇怪的

我们之前说过,浮点数 0.3 不等于 0.3。它实际上是:


>>> f"{0.3:.80f}"

'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

但是当你打印它时,为什么会显示0.3?

计算机实际上并没有打印出数字的精确值,而是打印出了最短的十进制数d,其中f是最接近d的浮点数。

事实证明,有效做到这一点很不简单,有很多关于它的学术论文,比如快速且准确地打印浮点数 legacy.cs.indiana.edu、如何准确打印浮点数 lists.nongnu.org等。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

如果计算机打印出浮点数的精确值,会不会更直观一些?

四舍五入到一个干净的十进制值很好,但在某种程度上,我觉得如果计算机只打印一个浮点数的精确值可能会更直观 -- 当你得到一个奇怪的结果时,它可能会让你看起来不那么惊讶。

对我来说,0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125比0.1 + 0.2 = 0.30000000000000000004惊讶少一点。

这也许是一个坏主意,因为它肯定会占用大量的屏幕空间。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

PHP 快速说明

有人在评论中指出在 PHP 中会输出0.3,这是否说明在 PHP 中浮点运算不一样?

非也 —— 我在这里 replit.com运行:

,得到了与 Python 完全相同的答案:5.5511151231258E-17。因此,浮点运算的基本原理是一样的。

我认为在 PHP 中0.1 + 0.2输出0.3的原因是 PHP 显示浮点数的算法没有 Python 精确 —— 即使这个数字不是最接近 0.3 的浮点数,它也会显示0.3。

78c8f172-fae3-11ed-90ce-dac502259ad0.svg

总结

我有点怀疑是否有人能耐心完成以上所有些算术,但它写出来对我很有帮助,所以我还是发表了这篇文章,希望它能有所帮助。

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 浮点数
    +关注

    关注

    0

    文章

    60

    浏览量

    15864
  • PHP
    PHP
    +关注

    关注

    0

    文章

    452

    浏览量

    26647

原文标题:为什么 0.1 + 0.2 = 0.30000000000000004?

文章出处:【微信号:良许Linux,微信公众号:良许Linux】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    verilog语音实现浮点运算

    Verilog可以通过使用IEEE标准的浮点数表示来实现浮点运算。下面是一个基本的Verilog模块示例,展示了如何进行加法、乘法和除法等常见的浮点运算操作: module
    发表于 03-25 21:49

    32位浮点加法器设计

    求助谁帮我设计一个32位浮点加法器,求助啊,谢谢啊 新搜刚学verilog,不会做{:4_106:}
    发表于 10-20 20:07

    为什么研究浮点加法运算,对FPGA实现方法很有必要?

    现代信号处理技术通常都需要进行大量高速浮点运算。由于浮点数系统操作比较复杂,需要专用硬件来完成相关的操作(在浮点运算中的浮点加法运算几乎占到
    发表于 07-05 06:21

    如何利用FPGA实现高速流水线浮点加法器研究?

    现代信号处理技术通常都需要进行大量高速浮点运算。由于浮点数系统操作比较复杂,需要专用硬件来完成相关的操作(在浮点运算中的浮点加法运算几乎占到
    发表于 08-15 08:00

    求一种在FPGA上实现单精度浮点加法运算的方法

    介绍一种在FPGA上实现的单精度浮点加法运算器,运算器算法的实现考虑了FPGA器件本身的特点,算法处理流程的拆分和模块的拆分,便于流水设计的实现。
    发表于 04-29 06:27

    请问一下高速流水线浮点加法器的FPGA怎么实现?

    请问一下高速流水线浮点加法器的FPGA怎么实现?
    发表于 05-07 06:44

    如何对计算加法电路进行proteus仿真呢

    计算加法的电路原理是什么?如何对计算加法电路进行proteus仿真呢?
    发表于 11-01 07:00

    高速流水线浮点加法器的FPGA实现

    高速流水线浮点加法器的FPGA实现 0  引言现代信号处理技术通常都需要进行大量高速浮点运算。由于浮点数系统操作比较复杂,需要专用硬件来完成相关的操
    发表于 02-04 10:50 2348次阅读
    高速流水线<b class='flag-5'>浮点</b><b class='flag-5'>加法</b>器的FPGA实现

    补码加法,补码加法计算原理

    补码加法,补码加法计算原理    负数用补码表示后,可以和正数一样来处理。这样,运算器里只需要一个加法器就可以了,不必为了负数的加法运算,再
    发表于 04-13 11:41 1.8w次阅读

    FPU加法器的设计与实现

    浮点运算器的核心运算部件是浮点加法器,它是实现浮点指令各种运算的基础,其设计优化对于提高浮点运算的速度和精度相当关键。文章从
    发表于 07-06 15:05 47次下载
    FPU<b class='flag-5'>加法</b>器的设计与实现

    十进制加法计算

    Multisim 仿真作业 一位十进制加法计算器。
    发表于 04-25 14:22 0次下载

    同相加法器电路原理与同相加法计算

    同相加法器输入阻抗高,输出阻抗低 反相加法器输入阻抗低,输出阻抗高.加法器是一种数位电路,其可进行数字的加法计算。当选用同相
    发表于 09-13 17:23 5.7w次阅读
    同相<b class='flag-5'>加法</b>器电路原理与同相<b class='flag-5'>加法</b>器<b class='flag-5'>计算</b>

    浮点运算单元的FPGA实现

    浮点加法是数字信号处理中的一种非常频繁且非常重要的操作,在现代数字信号处理应用中,浮点加法运算几乎占到全部浮点操作的一半以上。
    发表于 04-10 10:47 8次下载
    <b class='flag-5'>浮点</b>运算单元的FPGA实现

    怎么设计一个32bit浮点加法器呢?

    设计一个32bit浮点加法器,out = A + B,假设AB均为无符号位,或者换个说法都为正数。
    的头像 发表于 06-02 16:13 1195次阅读
    怎么设计一个32bit<b class='flag-5'>浮点</b>的<b class='flag-5'>加法</b>器呢?

    为什么研究浮点加法运算,对FPGA实现方法很有必要?

    浮点加法器是现代信号处理系统中最重要的部件之一。FPGA是当前数字电路研究开发的一种重要实现形式,它与全定制ASIC电路相比,具有开发周期短、成本低等优点。 但多数FPGA不支持浮点运算,这使FPGA在数值
    的头像 发表于 09-22 10:40 1016次阅读
    为什么研究<b class='flag-5'>浮点</b><b class='flag-5'>加法</b>运算,对FPGA实现方法很有必要?