可能你还不懂浮点数

本文介绍了浮点数在计算机中的表示方式,特别是通过IEEE 754标准,探讨了浮点误差产生的原因。由于二进制转换的限制,某些十进制小数无法精确表示为二进制,导致浮点运算存在误差。例如,0.1+0.2在二进制表示下不等于0.3。文章提出了两种处理浮点误差的方法:设定误差阈值和使用decimal模块进行十进制计算,并建议了解浮点数格式以更好地理解浮点误差。
摘要由CSDN通过智能技术生成

在网上看到一个问题

55f66ab4be50f3d0065ff25e8608eed5.png

然后看到这篇关于浮点数的文章,希望大家看了之后有所启发

想一下,为什么第一个打印的和预设值不同,但是第二个是相同的?

f274f59f61ab9725d407c535c871bef7.png

3b97e633bcc8432d1fd3ac18b58155df.png

如图:

尾数部分是如何转变成二进制的?

b014fa2c8dd0ee6411ad041e34c9a810.png

前言

很多人在初学写程式时都会遇到所谓的浮点误差,如果你到目前都还没被浮点误差雷过,那只能说你真的很幸运XD

以下图Python 的例子来说 0.1 + 0.2 并不等于0.38.7 / 10也不等于0.87,而是0.869999…,真的超怪der 🤔

58a9d69db2a38d1db4295428d2a81216.png

但这绝对不是什么神bug,也不是Python 设计得不好,而是浮点数在做运算时必然的结果,所以即便是到了Node.js 或其他语言也都是一样

26ffb81d94ca6ca3c854f24372e00629.png

电脑如何储存一个整数(Integer)

在讲为什么会有浮点误差之前,先来谈谈电脑是怎么用0 跟1 来表示一个 整数,大家应该都知道二进制这个东西:像 101 代表2² + 2⁰ 也就是5、1010代表2³ + 2¹ 也就是10

ae94104f6384a740329488b101daa241.png

如果是一个unsigned 的32 bit 整数,代表他有32 个位置可以放0 或1,所以最小值就是 0000...0000 也就是0,而最大值 1111...1111 代表2³¹ + 2³⁰ + … + 2¹ + 2⁰ 也就是4294967295

从排列组合的角度来想,因为每一个bit 都可以是0 或1,整个变数值有2³² 种可能性,所以可以 精确的 表达出0 到2³²-1 中任一个值,不会有任何误差

浮点数(Floating Point)

虽然从0 到2³²-1 之间有很多很多个整数,但数量终究是 有限 的,就是2³² 个那么多而已;但浮点数就大大的不同了,大家可以这样想:在1 到10 这个区间中只有十个整数,但却有 无限多个 浮点数,譬如说5.1、5.11、5.111 等等,再怎么数都数不完

但因为在32 bit 的空间中就只有2³² 种可能性,为了把所有浮点数都塞在这个32 bit 的空间里面,许多CPU 厂商发明了各种浮点数的表示方式,但若各家CPU 的格式都不一样也很麻烦,所以最后是以IEEE发布的IEEE 754作为通用的浮点数运算标准,后来的CPU 也都遵循这个标准进行设计

IEEE 754

IEEE 754 里面定义了很多东西,其中包括单精度(32 bit)、双精度(64 bit)跟特殊值(无穷大、NaN)的表示方式等等

正规化

以8.5 这个符点数来说,如果要变成IEEE 754 格式的话必须先做正规化:把8.5 拆成8 + 0.5 也就是2³ + 1/2¹,接着写成二进位变成1000.1,最后再写成1.0001 x 2³,跟十进位的科学记号满像的

单精度浮点数

在IEEE 754 中32 bit 浮点数被拆成三个部分,分别是sign、exponent 跟fraction,加起来总共是32 个bit

c55dd38c49f1134962b5e0ec49b2971a.png

  • sign:最左侧的1 bit 代表正负号,正数的话sign 就为0,反之则是 1

  • exponent:中间的8 bit 代表正规化后的次方数,采用的是 超127格式,也就是3 还要加上127 = 130

  • fraction:最右侧的23 bit 放的是小数部分,以1.0001 来说就是去掉1. 之后的0001

所以如果把8.5 表示成32 bit 格式的话就会是这样:

这图我画超久的,请大家仔细看XD

f0f6baa2986ce2eff3114a747b175ca0.png

什么情况下会不准呢?

刚刚8.5 的例子可以完全表示为2³+ 1/2¹,是因为8 跟0.5 刚好都是2 的次方数,所以完全不需要牺牲任何精准度

但如果是8.9 的话因为没办法换成2 的次方数相加,所以最后会被迫表示成1.0001110011… x 2³,而且还会产生大概0.0000003 的误差,好奇结果的话可以到IEEE-754 Floating Point Converter网站上玩玩看

双精度浮点数

上面讲的单精度浮点数只用了32 bit 来表示,为了让误差更小,IEEE 754 也定义了如何用64 bit 来表示浮点数,跟32 bit 比起来fraction 部分大了超过两倍,从23 bit 变成52 bit,所以精准度自然提高许多

107d20ba8ebccb4c11a021c8142dd30a.png

以刚刚不太准的8.9 为例,用64 bit 表示的话虽然可以变得更准,但因为8.9 无法完全写成2 的次方数相加,到了小数下16 位还是出现误差,不过跟原本的误差0.0000003 比起来已经小了很多

600f7ba7e62a6672ca01c787e5d5236d.png

类似的情况还有像Python 中的 1.0 跟 0.999...999 是相等的、123跟 122.999...999 也是相等的,因为他们之间的差距已经小到无法放在fraction 里面,所以就二进制的格式看来他们每一个bit 都一样

3907a5a94f52fc994f73e8c7b0389f95.png

解决方法

既然无法避免浮点误差,那就只好跟他共处了(打不过就加入?),这边提供两个比较常见的处理方法

设定最大允许误差ε (epsilon)

在某些语言里面会提供所谓的epsilon,用来让你判断是不是在浮点误差的允许范围内,以Python 来说epsilon 的值大概是2.2e-16

6b2c25b558a9e6f19773795bcb7e0395.png

所以你可以把 0.1 + 0.2 == 0.3 改写成0.1 + 0.2 — 0.3 <= epsilon,这样就能避免浮点误差在运算过程中作怪,也就可以正确比较出0.1 加0.2 是不是等于0.3

当然如果系统没提供的话你也可以自己定义一个epsilon,设定在2 的-15 次方左右

完全使用十进位进行计算

之所以会有浮点误差,是因为十进制转二进制的过程中没办法把所有的小数部分都塞进fraction,既然转换可能会有误差,那干脆就不要转了,直接用十进制来做计算!!

在Python 里面有一个module 叫做decimal,它可以帮你用十进位来进行计算,就像你自己用纸笔计算0.1 + 0.2 绝对不会出错、也不会有任何误差(其他语言也有类似的模组)

c7c9675703f9f86cf0a441fcd0b2f70b.png

自从我用了Decimal 之后不只bug 不见了,连考试也都考一百分了呢!

虽然用十进位进行计算可以完全躲掉浮点误差,但因为Decimal 的十进位计算是模拟出来的,在最底层的CPU 电路中还是用二进位在进行计算,所以跑起来会比原生的浮点运算慢非常多,所以也不建议全部的浮点运算都用Decimal 来做

总结

回归到这篇文章的主题:「为什么浮点误差是无法避免的?」,相信大家都已经知道了

至于你说知道IEEE 754 的浮点数格式有什么用吗?好像也没什么特别的用处XD,只是觉得能从浮点数的格式来探究误差的成因很有趣而已,感觉离真相又近了一点点

而且说不定哪天会有人问我「为什么浮点运算会产生误差而整数不会」,那时我就可以有自信的讲解给他听,而不是跟他说「反正浮点运算就是会有误差,背起来就对了」

后记

这是我第一次写这种几乎是纯原理的文章,不管你是觉得很有趣,还是觉得太理论了不知道学这要干嘛,都欢迎你在下方留言跟我说,或是透过拍手表达你的意见,这样我也比较能知道你们喜欢什么类型的文章,谢谢大家~

参考资料

  • https://zh.wikipedia.org/wiki/IEEE_754

  • https://www.h-schmidt.net/FloatConverter/IEEE754.html

  • https://zh.wikipedia.org/wiki/%E6%B5%AE%E7%82%B9%E6%95%B0

文章转自:https://medium.com/starbugs/see-why-floating-point-error-can-not-be-avoided-from-ieee-754-809720b32175

ac932bf0bbb975857d2eba7b0b9d526a.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当然可以,下面我来解释一下代码的主要部分。 首先,我们定义了两个变量 `sum` 和 `count`,用于统计班级总分和人数。初始值都为0。 ```python sum = 0 count = 0 ``` 然后,我们使用了一个while循环来不断输入每个同学的成绩。循环条件为 `True`,表示循环条件永远为真,因此需要在循环体内部使用 `break` 语句来跳出循环。 在循环体内部,我们先使用 `float(input())` 函数来读取用户输入的成绩,并将其转换为浮点数类型。如果用户输入的成绩小于0,说明输入结束,我们使用 `break` 语句跳出循环,进入后续的计算和输出。 如果用户输入的成绩大于等于0,说明输入的成绩有效,我们将其累加到 `sum` 变量中,并将 `count` 变量加1,表示班级人数加1。 ```python while True: score = float(input()) if score < 0: break sum += score count += 1 ``` 如果 `count` 变量的值为0,说明没有输入任何有效成绩,我们输出 "人数为0"。 ```python if count == 0: print("人数为0") ``` 如果 `count` 变量的值大于0,说明已经输入了有效成绩,我们计算平均分并输出。平均分的计算公式为 `avg = sum / count`,其中 `sum` 表示班级总分,`count` 表示班级人数。为了保留两位小数,我们使用了字符串格式化函数 `"{:.2f}".format(avg)`。 ```python else: avg = sum / count print("{:.2f}".format(avg)) ``` 希望这样的解释能帮助你理解这段代码。如果还有什么不的地方,可以随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值