Python_进阶_浮点型(float)不可避免的误差产生原因,float类型在所有计算机中的储存方式——获得图灵奖的天才设计:IEEE754 标准

Python_进阶_浮点型(float)不可避免的误差产生原因,float类型在所有计算机中的储存方式——获得图灵奖的天才设计:IEEE754 标准

目录

说明

一、前言

二、理论

2.1 Bug

2.2 十进制小数转换为二进制小数

2.3 IEEE754标准

2.4 IEEE754标准实际储存的格式

2.4.1 通用情况

2.4.2 特殊情况

2.4.3 规格化浮点数与非规格化浮点数

2.4.4 通用储存格式

2.5 二进制舍入原理

三、计算结果验证

3.1 验证

3.2 误差消除方法

四、整体代码及注释

五、结语

五、参考


说明

本文仅作为学习分享

本文在描述IEEE754 标准时,仅描述了双精度浮点型(64位)的情况。但单精度浮点型与其理论相同,所以不再赘述。

关键词:

IEEE754标准;Float;Python;二进制舍入

一、前言

在学习计算机语言时,经常会遇到一个很经典的BUG——计算机在计算小数时一定会产生误差,特别是在科学计算时。

本科时的老师就让我们有兴趣的话课下可以学习一下这方面的知识,但当时仅仅只是浅显的了解了一下。直到这两天才深入学习了这方面的知识。

本来我以为这是一个很小的知识点,但学习之后才发现这是一个非常重要的系统化的知识。

深入学习非常有助于理解计算机和计算机语言,也会对软件和硬件的关系有更深刻的认识。

IEEE754 标准是获得图灵奖的天才设计,本文将较为详细的描述这个浮点数处理的基石。

二、理论

2.1 Bug

在python3.0中,如果我们直接将两个小数相加,大概率是个错误结果

图2-1 BUG例程
图2-1 BUG例程

0.1+0.2的结果明明是0.3,可是计算机却出现了这种显而易见的低级错误

这明显是错误的,可是是什么导致了这个错误呢?

我们大致需要以下知识:

1:十进制小数转换为二进制

2:IEEE754标准

3:二进制舍入方法

2.2 十进制小数转换为二进制小数

十进制小数转换为二进制小数的方法是——整数部分展除二取余并逆位读数,小数部分乘二取整并顺位读数。

这方面知识不会的请去查阅,在此不再赘述。

例如3.5这个小数

计算结果为整数部分11,小数部分1。所以整体为:0b11.1

现在验证一下

图2-2 验证进制转换

结果正确

2.3 IEEE754标准

现在简单介绍一下电气电子工程师学会(IEEE)754浮点数储存标准

IEEE 754是计算机中表示浮点数的通用标准,定义了二进制浮点数的格式、精度、舍入规则及异常处理方式。该标准广泛应用于硬件和软件实现,确保浮点数运算的跨平台一致性。

IEEE754标准可以表达为:(-1)^Sign * Base^Exponent  * Mantissa

其中“*”为乘号
其中“^”为指数符号 (在python中为“**”)
其中Sign为正负标志
其中Mantissa为尾数,或者说有效数值,这个数值必须且仅保留一位正数位
其中Base为基数,或者说进制
其中Exponent为指数,或者说小数进位数

此标准适用于所有浮点数,包括软件和硬件

看不懂没关系,下面会举例

2.4 IEEE754标准实际储存的格式

上文的表达式是IEEE754标准的表达式,但和实际储存方式有细微的差别

2.4.1 通用情况

        Sign符号位很简单,正数为0,负数为一
        Base更简单,这是在确定进制之后的固定值,计算机储存一般为2
        Mantissa也很简单,将有效数值小数点后的数值储存就行,因为规格化浮点数的小数正数部分一定为一(规格化浮点数后面解释)
        但在储存Exponent时有一个问题,如果指数是负数,那么需要一个额外的符号位去表示指数的正负
在python中,也就是双精度浮点型标准下,实际指数储存时会加上11位指数位最大值的一半,也就是储存的Exponent_input=Exponent+1023
我们将这个指数位最大值的一半1023称之为偏移量
通过加上这个偏移量,可以直接使得储存的所有指数的值皆大于等于零,不需要额外的符号位
那么显而易见的是,理论上11位指数位0-2047可以表示-1023—+1024这2048个指数值

2.4.2 特殊情况

IEEE754标准规定浮点数在储存指数时保留两种特殊情况

1:指数部分全为1时
        若尾数部分为零,则此时表示无穷大
                且若符号位为1时,则表示负无穷大
                且若符号位为0时,则表示正无穷大
        若尾数部分不为零,则表示NaN(Not a Number),即无效数字
2:指数部分全为零时,表示这是一个非规格化浮点数

2.4.3 规格化浮点数与非规格化浮点数

我们先来解释什么是规格化浮点数

python在储存尾数时,直接舍弃了整数部分的一位,因为正常情况下浮点数二进制储存的正数位一定是1,所以我们可以节省下这一位的空间

在这种正数位为1的情况下的浮点数,我们称之为规格化浮点数

规格化浮点数在计算原始指数时,只需要将储存值减去偏移量,即Exponent=Exponent_input-1023

但在非规格化浮点数的情况下,也就是指数为零时,尾数舍弃的正数部分变为了0

这时的原始指数Exponent=Exponent_input-1023这个公式不再适用,因为非规格化浮点数的正数部分不合规格,即无有效的正数部分

此时,原始指数Exponent=1-1023,恒等于-1022
此时,又会出现特殊情况,即尾数为0
IEEE754标准规定:当指数为零,尾数为零时,值为0
且当符号为正时,表示0+
且当符号为负时,表示0-

可是,为什么要这么做呢?这不是看起来多此一举吗?

其实答案就在默认隐含掉的正数部分上

如果我们不引入非规格化浮点数,那么就默认尾数的正数部分为1

此时最小数为1.0*2**(-1022)

也就是说此时,我们虽然指数的数量级最小了,但尾数完全不是最小数量级

我们浪费掉了52位尾数位的数量级

但如果引入非规格化浮点数之后,隐含掉的正数部分变为了0

于是此时我们可以完全利用尾数的52位

此时的最小数为2**(-52)*2**(-1022),整整差了52个数量级

通过上述的内容,我们也能得出一个结论,双精度浮点数的精度为2**(-1074)

2.4.4 通用储存格式

经过上文的论述,我们已经对python双精度浮点数的储存模式有了清晰的概念

本文只描述规格化浮点数的情况,但其实非规格化浮点数,0,无穷大和NaN原理相同,读者可以自行推导。

python中以二进制储存(或者说绝大部分的计算机语言都用二进制储存),那么基数是确定的,也就是2。
而python舍弃了整数部分的1位,所以实际的尾数为Mantissa-1,仅保留小数部分
对于规格化浮点数,Exponent_input=Exponent+1023
所以python的浮点数表达式为:(-1)**Sign * Mantissa * (2**Exponent)
实际储存时,从高到低的数据格式为:sign[0]Exponent[10:0]Mantissa[51:0]
实际储存时,从高到低的实际数据为:[sign][Exponent+1023][Mantissa-1]

说起来有点复杂,但其实很简单,现在我举个例子

例如上文中的小数3.5,二进制为:0b11.1

首先它是个正数,那么S为0

有效值的进位为1,所以Exponent为1+1023=1024,1024转换为二进制为100 0000 0000

它的有效数值为1.11,那么Mantissa=1.11,我们只取小数点后的有效数值11

将他们拼接起来,所以小数3.5在python中实际储存为:

0_10000000000_1100000000000000000000000000000000000000000000000000

现在我们验证一下

图2-3 3.5实际储存验证

计算与结果完全一致

2.5 二进制舍入原理

现在我们回到最开始的问题,为什么会出现那个Bug

实际上,除了小数点后最后一位为5的所有小数,在计算机中都并不精准

从我们刚刚学习的浮点数储存方式中,我们知道小数部分是用乘二取整法转换的

那么python中储存0.1的实际数据是什么呢?

理论上,0.1转换为二进制为0.0001100110011....0011

这是一个无限循环

根据上文的推导,可以简单得出sign=0,exponent=-4

故exponent_input=-4+1023=1019,转换为二进制为001111111011

mantissa=1.100110011....0011,舍去正数位为100110011....0011

故整体储存为:0_001111111011_100110011....

但是我们知道,我们只有52位尾数位,所以必然不是精确数值

在python3.0中,float储存遵循的是IEEE754标准中的“最近舍入,平局则偶”

意思是尾数的舍入选取离实际数值最近的那个数值,如果有两个数值离实际数值一样近,则选择偶数数值(十进制下的最后一位)

但是如何在计算机的二进制中实现这个目标呢?

接下来我将介绍另一个天才设计—二进制“最近舍入,平局则偶”舍入方法

这个方法需要三个标志位:

保护位(Guard bit, G):紧跟在有效数字最低位之后的第一位。
舍入位(Round bit, R):保护位之后的第二位。
粘滞位(Sticky bit, S):舍入位之后的所有剩余位经过逻辑或运算得到的一个标志位(只要剩余位中有1,S就为1;全为0则S为0)

通过 G、R、S 三个位的组合,可以准确判断中间结果与两个最近可表示浮点数的距离关系

情况一:G=0
则说明靠近较小的数,向下舍入
情况二:G=1 且 (R或S=1)
则说明靠近较大的数,向上舍入
情况三:G=1 且 R=0 且 S=0
平局
舍入结果:根据目标有效数字的最低位(LSB)决定:
如果 LSB = 0(偶数),则向下舍入(保持最低位为 0);
如果 LSB = 1(奇数),则向上舍入(使最低位变为 0,因为加 1 后最低位变为 0,可能进位到更高位)

我们可以简单得出,0.1的最后一字节数据为1001,后续数据为:1001001001....

故G=1,R=0,S=1

那么根据以上原则,应向上舍入,即将1001舍入为1010

所以,整体数据为:
0_01111111011_1001100110011001100110011001100110011001100110011010

三、计算结果验证

3.1 验证

我们先验证一下0.1的计算推导结果

图3-1 0.1实际储存验证

计算与结果完全相等

经过计算,这个数值实际为

0.1000000000000000055511151231257827021181583404541015625

同理可得,0.3在储存中的实际数值为

0.200000000000000011102230246251565404236316680908203125

在python中将这两个数值相加

图3-2 和验证

与预期一致

3.2 误差消除方法

想要消除这种误差,实际使用python等计算机语言时有很多方法可以做到

这些方法都极其简单,比如decimal模块、fractions模块、使用整数运算(缩放法)、比较时使用容差(math.isclose)、格式化输出时控制小数位数等

在此不再赘述

当然,这也是一个很好的课题

四、整体代码及注释

#接下来我们看一个非常经典的计算机蝽(Bug) float_num_error=0.1+0.2 print(float_num_error) #输出结果为0.30000000000000004 #这明显是错误的,可是是什么导致了这个错误呢? #我们需要以下知识: #1:十进制整数即小数部分转换为二进制 #2:IEEE754双精度浮点数(64位)储存标准 #首先我们先解释十进制小数转换为二进制小数 #十进制小数转换为二进制小数的方法是——整数部分展除二取余并逆位读数,小数部分乘二取整并顺位读数 #这方面知识不会的请去查阅,在此不再赘述 #例如3.5 #计算结果为整数部分11,小数部分1。所以整体为:0b11.1 #现在验证一下 float_num=0b11+0b1/(1<<1)#python无法直接表达二进制小数,故采用组合方式,新手不用管 print(float_num) #结果正确 #现在简单解释一下电气电子工程师学会(IEEE)754双精度浮点数(64位)储存标准 #IEEE754标准可以表达为:(-1)**Sign * Mantissa * Base**Exponent #其中“*”为乘号 #其中“**”为指数符号 #其中Sign为正负标志 #其中Mantissa为尾数,或者说有效数值,这个数值必须且仅保留一位正数位 #其中Base为基数,或者说进制 #其中Exponent为指数,或者说小数进位数 #现在的问题是,python等计算机实际储存的格式 #Sign符号位很简单,正数为0,负数为一 #Base更简单,这是在确定进制之后的固定值,计算机储存一般为2 #Mantissa也很简单,将有效数值小数点后的数值储存就行 #但在储存Exponent时有一个问题,如果指数是负数,那么需要一个额外的符号位去表示指数的正负 #在python中,实际指数储存时会加上11位指数位最大值的一半,也就是储存的Exponent_input=Exponent+1023 #我们将这个指数位最大值的一半1023称之为偏移量 #通过加上这个偏移量,可以直接使得储存的所有指数的值皆大于等于零,不需要额外的符号位 #那么显而易见的是,理论上11位指数位0-2047可以表示-1023—+1024这2048个指数值 #但是IEEE754标准规定浮点数在储存指数时保留两种特殊情况 #1:指数部分全为1时 #若尾数部分为零,则此时表示无穷大 #且若符号位为1时,则表示负无穷大 #且若符号位为0时,则表示正无穷大 #若尾数部分不为零,则表示NaN(Not a Number),即无效数字 #2:指数部分全为零时,表示这是一个非规格化浮点数 #我们先来解释什么是规格化浮点数 #python在储存尾数时,直接舍弃了整数部分的一位,因为正常的浮点数二进制储存的正数位一定是1,所以我们可以节省下这一位的空间 #在这种正数位为1的情况下的浮点数,我们称之为规格化浮点数 #规格化浮点数在计算原始指数时,只需要将储存值减去偏移量,即Exponent=Exponent_input-1023 #但当指数为零时,尾数舍弃的正数部分变为了0 #这时的原始指数Exponent=Exponent_input-1023这个公式不再适用,因为非规格化浮点数的正数部分不合规格,即无有效的正数部分 #此时,原始指数Exponent=1-1023,恒等于-1022 #此时,又会出现特殊情况,即尾数为0 #IEEE754标准规定:当指数为零,尾数为零时,值为0 #且当符号为正时,表示0+ #当符号为负时,表示0- #为什么要引入非规格化浮点数?其实答案就在隐含掉的正数部分上 #如果我们不引入非规格化浮点数,那么就默认尾数的正数部分为1 #此时最小数为1.0*2**(-1022) #也就是说此时,我们虽然指数的数量级最小了,但尾数完全不是最小数量级 #我们浪费掉了52位尾数位的数量级 #但如果引入非规格化浮点数之后,隐含掉的正数部分变为了0 #于是此时我们可以完全利用尾数的52位 #此时的最小数为2**(-52)*2**(-1022) #整整差了52个数量级 #通过上述的内容,我们也能得出一个结论,双精度浮点数的精度为2**(-1074) #经过上文的论述,我们已经对python双精度浮点数的储存模式有了清晰的概念 #python中以二进制储存(或者说绝大部分的计算机语言都用二进制储存),那么基数是确定的,也就是2。 #而python舍弃了整数部分的1位,所以实际的尾数为Mantissa-1 #在此本文只描述规格化浮点数的情况 #对于规格化浮点数,Exponent_input=Exponent+1023 #所以python的浮点数表达式为:(-1)**Sign * Mantissa * (2**Exponent) #实际储存时,从高到低的数据格式为:sign[0]Exponent[10:0]Mantissa[51:0] #实际储存时,从高到低的实际数据为:[sign][Exponent+1023][Mantissa-1] #说起来很复杂,但其实很简单,现在我举个例子 #例如上文中的小数3.5,二进制为:0b11.1 #首先它是个正数,那么S为0 #它的有效数值为1.11,那么Mantissa=1.11,且Mantissa-1=0.11,我们只取小数点后的有效数值11 #它的进位为1,所以Exponent为1+1023=1024,1024转换为二进制为100 0000 0000 #将他们拼接起来,所以小数3.5在python中实际储存为:0_10000000000_1100000000000000000000000000000000000000000000000000 #现在我们验证一下 import struct def float_to_binary64(f): #将浮点数打包为8字节(默认使用本机字节序,通常为小端) bytes_=struct.pack('d',f) #将每个字节转换为8位二进制并拼接并转换为大端序.join(f'{b:08b}' for b in struct.pack('>d', f)) return bits f = 3.5 #将任何你想测试的值填入f bits = float_to_binary64(f) print(bits) #计算与结果完全一致 #现在我们回到最开始的问题,为什么会出现那个Bug #实际上,除了小数点后最后一位为5的所有小数,在计算机中都并不精准 #从我们刚刚学习的浮点数储存方式中,我们知道小数部分是用乘二取整法转换的 #那么python中储存0.1的实际数据是什么呢? #理论上,0.1转换为二进制为0.0001100110011....0011 #这是一个无限循环 #根据上文的推导,可以简单得出sign=0,exponent=-4 #故exponent_input=-4+1023=1019,转换为二进制为001111111011 #mantissa=1.100110011....0011,舍去正数位为100110011....0011 #故整体储存为:0_001111111011_100110011.... #但是我们知道,我们只有52位尾数位,所以必然不是精确数值 #在python3.0中,float储存遵循的是IEEE754标准中的“最近舍入,平局则偶” #意思是尾数的舍入选取离实际数值最近的那个数值,如果有两个数值离实际数值一样近,则选择偶数数值(十进制下的最后一位) #但是如何在计算机的二进制中实现这个目标呢? #接下来我将介绍另一个天才设计—二进制“最近舍入,平局则偶”舍入方法 #这个方法需要三个标志位: #保护位(Guard bit, G):紧跟在有效数字最低位之后的第一位。 #舍入位(Round bit, R):保护位之后的第二位。 #粘滞位(Sticky bit, S):舍入位之后的所有剩余位经过逻辑或运算得到的一个标志位(只要剩余位中有1,S就为1;全为0则S为0)。 #通过 G、R、S 三个位的组合,可以准确判断中间结果与两个最近可表示浮点数的距离关系 #情况一:G=0 #则说明靠近较小的数,向下舍入 #情况二:G=1 且 (R或S=1) #则说明靠近较大的数,向上舍入 #情况三:G=1 且 R=0 且 S=0 #平局 #舍入结果:根据目标有效数字的最低位(LSB)决定: #如果 LSB = 0(偶数),则向下舍入(保持最低位为 0); #如果 LSB = 1(奇数),则向上舍入(使最低位变为 0,因为加 1 后最低位变为 0,可能进位到更高位)。 #我们可以简单得出,0.1的最后一字节数据为1001,后续数据为:1001001001.... #故G=1,R=0,S=1 #那么根据以上原则,应向上舍入,即将1001舍入为1010 #所以,整体数据为——0_01111111011_1001100110011001100110011001100110011001100110011010 #验证 import struct def float_to_binary64(f): #将浮点数打包为8字节(默认使用本机字节序,通常为小端) bytes_=struct.pack('d',f) #将每个字节转换为8位二进制并拼接并转换为大端序.join(f'{b:08b}' for b in struct.pack('>d', f)) return bits f = 0.1 #将任何你想测试的值填入f bits = float_to_binary64(f) print(bits) #计算与结果完全相等 #经过计算,这个数值实际为0.1000000000000000055511151231257827021181583404541015625 #同理可得,0.3在储存中的实际数值为0.200000000000000011102230246251565404236316680908203125 #将这两个数值相加 float_num=0.1000000000000000055511151231257827021181583404541015625+0.200000000000000011102230246251565404236316680908203125 print(float_num) #与预期一致 #想要消除这种误差,有很多方法可以做到 #这些方法都极其简单,比如decimal模块、fractions模块、使用整数运算(缩放法)、比较时使用容差(math.isclose)、格式化输出时控制小数位数等 #在此不再赘述 #但最重要的是,要理解这种误差,并且平衡效能(速度)和误差之间的关系。有的时候,没有必要为了无效的精准度而牺牲效率

五、结语

IEEE754标准,这个浮点数的基石,我认为诠释了电子数学的美。

电子数学与数学的区别是,我们不得不面对物理、器件、技术、材料等的限制。电子技术没有最好,只有更好

我们必须在精度、范围和效率之间找到一个平衡点

就像生活一样不是吗?

本躯能力不足,若有错误恳请斧正

五、参考

  1.  C语言中文网 n.d., '小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计(长篇神文)', C语言中文网, viewed 28 February 2026, http://c.biancheng.net/
  2.  ZEEKLOG博客 2024, '控制python中的浮点精度', 腾讯云开发者社区, viewed 28 February 2026, https://cloud.tencent.cn/developer/information/控制python中的浮点精度
  3.  Oracle Corporation 2016, Numerical Computation Guide, part number E71940-01, Oracle Developer Studio 12.5, viewed 28 February 2026, https://docs.oracle.com/cd/E71940_01/html/E71991/z4000ac019878.html
  4. Stack Overflow 2014, 'Python3 rounding to nearest even', Stack Overflow, viewed 28 February 2026, https://stackoverflow.com/questions/23248489
  5. 科普中国 2019, '双精度浮点数', 科普中国·科学百科, viewed 28 February 2026, https://cloud.kepuchina.cn/newSearch/imgText?from=1&id=6974140214012293121

Read more

深入解析 KES 数据库运维核心:资源回收与膨胀防治全攻略

深入解析 KES 数据库运维核心:资源回收与膨胀防治全攻略

在数据库长期运行过程中,表膨胀与索引膨胀是 KingbaseES(KES)DBA 最常面对的"隐形杀手"。它们悄无声息地蚕食磁盘空间、拖慢查询性能,严重时甚至威胁系统稳定性。本文从索引重建、垃圾回收原理、长事务阻断、autovacuum 精细化调优四个维度,系统梳理 KES 资源回收的核心机制与实战方法。 一、REINDEX CONCURRENTLY:不停机重建膨胀索引 随着业务 DML 语句持续增长,索引会像表一样发生膨胀。膨胀的索引不仅浪费磁盘空间,还会显著降低查询性能——新构建的索引往往比反复更新的旧索引提供更好的访问效率。 为什么不能直接用 REINDEX? 普通 REINDEX 命令需要 ACCESS EXCLUSIVE 锁,这是最高级别的锁,会阻塞一切业务语句,生产环境中几乎不可接受。 解决方案是使用 REINDEX ... CONCURRENTLY,其锁级别降为 SHARE UPDATE EXCLUSIVE,不阻塞

By Ne0inhk
浏览器自动化新范式:深度体验 OpenClaw 驱动的 AI 网页操作

浏览器自动化新范式:深度体验 OpenClaw 驱动的 AI 网页操作

目录 浏览器自动化新范式:深度体验 OpenClaw 驱动的 AI 网页操作 🛠️ 核心配置:打通 AI 与浏览器的“隧道” 1. 配置文件 (openclaw.json) 2. 插件连接 🤖 实战:微博数据自动化整理 核心 Prompt 示例: 🔍 深度思考:OpenClaw 的优势与局限 🌟 优势 ⚠️ 局限(划重点!) 💡 总结 浏览器自动化新范式:深度体验 OpenClaw 驱动的 AI 网页操作 在 AI 智能体(Agent)爆发的今天,让 AI 像人一样操作浏览器已不再是科幻。近日,我深度体验了开源项目 OpenClaw,通过其 Browser Relay

By Ne0inhk
Flutter for OpenHarmony 实战:Injectable — 自动化依赖注入大师

Flutter for OpenHarmony 实战:Injectable — 自动化依赖注入大师

Flutter for OpenHarmony 实战:Injectable — 自动化依赖注入大师 前言 在维护 Flutter for OpenHarmony 商业级项目时,由于功能重叠与模块解耦的需求,代码库中会充斥着大量的 Service(业务服务)、Repository(数据中心)及 Bloc/ViewModel。如果采用手动实例化这些类并逐层透传,代码会迅速演变成不可维护的“意大利面条”。 依赖注入 (Dependency Injection, DI) 是解决该问题的业界公认方案。而 Injectable 配合 GetIt,则是 Dart 生态中实现 DI 的皇冠。它能通过极其简洁的注解,在编译期自动生成复杂的注册代码。本文将带你探索如何利用 Injectable 构建一个灵活适配鸿蒙多运行环境的高级架构。 一、为什么 Injectable 是鸿蒙项目的必选项? 1.1 依赖管理的解耦

By Ne0inhk
Linux 进阶:一文搞懂 make 工具与 Makefile 编写

Linux 进阶:一文搞懂 make 工具与 Makefile 编写

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 一、先搞懂:make 是什么?Makefile 又是什么? 1.1 背景 1.2 make:Linux 下的自动化构建工具 1.3 Makefile:make 的 “操作手册” 二、最佳实践:先见一下,如何使用 2.1 构建项目 2.2 清理项目 2.3 理解Makefile/make,

By Ne0inhk