在网上看到一个问题
然后看到这篇关于浮点数的文章,希望大家看了之后有所启发。
想一下,为什么第一个打印的和预设值不同,但是第二个是相同的?
如图:
尾数部分是如何转变成二进制的?
前言
很多人在初学写程式时都会遇到所谓的浮点误差,如果你到目前都还没被浮点误差雷过,那只能说你真的很幸运XD。
以下图Python 的例子来说 0.1 + 0.2
并不等于0.3
,8.7 / 10
也不等于0.87
,而是0.869999…
,真的超怪der 🤔
但这绝对不是什么神bug,也不是Python 设计得不好,而是浮点数在做运算时必然的结果,所以即便是到了Node.js 或其他语言也都是一样。
在讲为什么会有浮点误差之前,先来谈谈电脑是怎么用0 跟1 来表示一个 整数,大家应该都知道二进制这个东西:像 101
代表2² + 2⁰ 也就是5、1010
代表2³ + 2¹ 也就是10。
如果是一个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。
sign:最左侧的1 bit 代表正负号,正数的话sign 就为0,反之则是 1。
exponent:中间的8 bit 代表正规化后的次方数,采用的是 超127格式,也就是3 还要加上127 = 130。
fraction:最右侧的23 bit 放的是小数部分,以1.0001 来说就是去掉1. 之后的000。
所以如果把8.5 表示成32 bit 格式的话就会是这样:
这图我画超久的,请大家仔细看XD。
刚刚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,所以精准度自然提高许多。
以刚刚不太准的8.9 为例,用64 bit 表示的话虽然可以变得更准,但因为8.9 无法完全写成2 的次方数相加,到了小数下16 位还是出现误差,不过跟原本的误差0.0000003 比起来已经小了很多。
类似的情况还有像Python 中的 1.0
跟 0.999...999
是相等的、123
跟 122.999...999
也是相等的,因为他们之间的差距已经小到无法放在fraction 里面,所以就二进制的格式看来他们每一个bit 都一样。
既然无法避免浮点误差,那就只好跟他共处了(打不过就加入?),这边提供两个比较常见的处理方法。
在某些语言里面会提供所谓的epsilon,用来让你判断是不是在浮点误差的允许范围内,以Python 来说epsilon 的值大概是2.2e-16。
所以你可以把 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 绝对不会出错、也不会有任何误差(其他语言也有类似的模组)。
自从我用了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
1.SylixOS,这款嵌入式RTOS值得学习!
2.芯片短缺或将结束?数据来说话。。。
3.代码调着调着就失联了,你碰到过吗?
4.图文并茂详解STM32时钟配置
5.电力物联网与嵌入式系统,关系几何?
6.RT-Thread携手北航出版社共办人工智能师资培训
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。