为什么 0.1 + 0.2 不等于 0.3?这篇文章,我们将通过这个示例来分析浮点数在计算机中是如何存储的?
一、定点数
1. 定义
定点数,比较简单,从字面上理解为小数点固定的数。比如,100,3.14,200.08等等都可以看成是定点数。
通常意义上,定点数表示整数或小数,可以分为以下三种情况:
- 纯整数:例如,400,小数点在最后一位,可以忽略
- 纯小数:例如,0.68,小数点固定在最高位
- 整数+小数:例如,3.14、9.18,小数点在指定某个位置
接下来,我们一起看看定点数的十进制和二进制的相互转换。
2. 十进制整数转二进制
将十进制数转换为二进制数,通过不断地除以 2,直到商为 0。步骤:
- 将十进制数除以2,记录商和余数
- 将商再次除以2,记录新的商和余数
- 重复这个过程,直到商为 0为止
- 从下往上读取所有的余数,就得到了转换后的二进制数
比如:将十进制的 38转换成二进制:
38 / 2 = 19 余 0
19 / 2 = 9 余 1
9 / 2 = 4 余 1
4 / 2 = 2 余 0
2 / 2 = 1 余 0
1 / 2 = 0 余 1
所以,(38)₁₀ 的二进制是 (100110)₂
3. 二进制整数转十进制
将二进制数转换为十进制数,根据权值展开法,将每一位上的数字与其对应的权值相乘,然后将所有结果相加。权值是 2的幂,从右向左依次增加。
例如,将二进制数 100110转换成十进制:
1*2⁵ + 0*2⁴ + 0*2³ + 1*2² + 1*2¹ + 0*2⁰
= 32 + 0 + 0 + 4 + 2 + 0
= 38
所以,(100110)₂ 的十进制是 (38)₁₀
4. 十进制小数转二进制
十进制小数转二进制分两部分:整数部分转换上面已经讲解了,小数部分采用“乘2取整,从上到下顺序排列”。
例如,十进制小数 10.75 转为二进制小数:
# 整数部分
10 / 2 = 5 余 0
5 / 2 = 2 余 1
2 / 2 = 1 余 0
1 / 2 = 0 余 1
# 小数部分
0.75 * 2 = 1.5 取整数部分 1
0.5 * 2 = 1.0 取整数部分 1
所以,(10.75)₁₀ 的二进制是 (1010.11)₂
5 二进制小数转十进制
十进制小数转二进制分两部分,整数部分转为二进制上面已经讲解了,小数部分采用“乘2取整,从上到下顺序排列”。
例如,二进制小数转十进制小数:
# 整数部分
1*2³ + 0*2² + 1*2¹ + 0*2⁰
= 8 + 0 + 2 + 0 + 2 + 0 = 10
# 小数部分
1*2⁻¹ + 1*2⁻²
= 0.5 + 0.25
= 0.75
# 整数部分 + 小数部分
10 + 0.75 = 10.7
所以,(1010.11)₂ 的十进制是 (10.75)₁₀
6. 定点数的优缺点
(1) 优点
定点数的精度是固定的,可以根据需求进行灵活调整,这使得它们在需要固定精度的应用中非常有用。
(2) 缺点
定点数的精度是固定的,这意味着在处理大范围或者需要高精度的数据时,可能会丢失精度或者溢出。
比如,以 1个字节(8 bit)为例,假如约定前 4位表示整数部分,后 4位表示小数部分,因此,可以表达的最大数是:(1111.1111)₂=(15.9375)₁₀。
如果想要表示更大范围的值,怎么办?
- 增加 bit:比如,使用 2个字节(16 bit)、4个字节(32bit),这样整数部分和小数部分都增加了,表示的数字范围也变大了。
- 小数点右移:小数点右移,整数范围就变大了,因此整个数字范围就变大了,但是小数部分的精度就会越来越低。
因此,对于表示大范围或者高精度的数,定点数存在其局限性,这时候“浮点数”就派上用场了。
二、IEEE 754
在讲解浮点数之前,我们需要先了解一个很重要的标准:IEEE 754,它是 20世纪80年代以来最广泛使用的浮点数运算标准,为许多 CPU、浮点运算器和编程语言(比如 Java)所采用。
这个标准定义了以下规范:
- 表示浮点数的格式(包括负零-0)与反常值(Denormal number);
- 一些特殊数值(无穷 Inf与非数值 NaN),以及这些数值的浮点数运算;
- 指明了四种数值修约规则和五种例外状况(包括例外发生的时机与处理方式);
另外,在 IEEE 754标准推出之前,各个计算机公司对于浮点数的表示没有一个业界通用的标准,这给数据交换、计算机协同工作造成了极大不便,直到 1985年,IEEE 组织推出了 IEEE 754浮点数标准,才结束了这混乱的局面。
三、浮点数
1. 什么是浮点数?
浮点数,是相对定点数而言的,从字面上可以解释为:小数点在浮动的数。在计算机科学中,浮点数是一种用于表示带有小数点的实数的数据类型,通常采用了科学计数法表示。
如下例子,十进制小数 300.14,用科学计数法表示,可以有多种方式,整体看上去,小数点好像在浮动。
300.14 = 30.014 × 10¹
300.14 = 3.0014 × 10²
300.14 = 0.31004 × 10³
在 IEEE 754标准中,浮点数在内存中以二进制形式存储,分为四个部分:符号位、基数、指数部分和尾数部分。
- 符号位(Sign bit):用于表示数值的正负。0表示正数,1表示负数。
- 指数部分(Exponent):用于表示数值的大小范围。该部分存储的是一个无符号整数,通常采用偏移表示,即用实际指数加上一个固定偏移值。
- 基数(Base):也称为进制或底数,常见的基数包括十进制(基数为10)、二进制(基数为2)、八进制(基数为8)、十六进制(基数为16)等。
- 尾数部分(Mantissa/Significand):用于表示数值的精度和小数部分。在IEEE 754中,尾数总是处于[1,2)之间的一个小数,这样可以省略掉小数点前面的 1,从而节省了一个 bit。注意:significand 和 mantissa 都是用来指代浮点数中的尾数部分,只不过 mantissa是早期计算机的叫法。两个术语可以互换,用来表示浮点数中的尾数部分。
下图以 300.14为例展示了一个浮点数的构成:
2. 浮点数的精度
在IEEE 754标准中规定了 4种主要的浮点数类型:单精度浮点数(float)、双精度浮点数(double)、延伸单精确度以及延伸双精确度。
(1) 单精度浮点数(float)
单精度浮点数占用 32位(4个字节),其中,符号占 1位,指数部分占 8位,尾数部分占 23位。指数部分可以表示的范围是 0~255,因为存在偏移量,因此指数的实际范围是从-126~+127,即 2⁻¹²⁶~2¹²⁷,转换成十进制,范围大约为 ±3.4x10⁻³⁸ 到 ±3.4x10³⁸之间。
(2) 双精度浮点数(double)
双精度浮点数占用 64位(8个字节),其中,符号占 1位,指数部分占 11位,尾数部分占 52位。指数部分可以表示的范围是 0~2047,因为存在偏移量,因此指数的实际范围是从-1022~+1023,即 2⁻¹⁰²²~ 2¹⁰²³,转换成十进制,范围大约为 ±1.7x10⁻³⁰⁸ 到 ±1.7x10³⁰⁸之间。
(3) 延伸单精确度
因为不常用,所以本文不进行讲解。
(4) 延伸双精确度
因为不常用,所以本文不进行讲解。
关于单精度浮点数和双精度浮点数的二进制表示如下图:
在了解完定点数和浮点数的一些基本概念之后,接下来讲解浮点数使用IEEE 754标准在内存是如何使用二进制存储的, 这里是IEEE 754标准的精华部分,也是比较难理解的部分,我会通过实际的例子结合图形进行分析。
3. 浮点数的IEEE754转换
(1) 十进制转二进制
这里以十进制转 IEEE754单精度浮点数并且无精度丢失为例,核心流程包含 5个步骤:
① 确认 Sign符号位
比如,19.59375的符号为 0(正数),-1.1的符号为 1(负数),将结果值(0/1)填充到二进制的 Sign bit区域。
② 将十进制转成纯二进制
这个步骤,只是纯粹地将十进制转换成二进制。
- 对于整数部分,通过不断地除以 2,直到商为 0;
- 对于小数部分,采用逐步乘 2取整,直到小数部分为 0或达到尾数所需的精度;
需要注意,对于小数部分处理结束的条件有 2个:小数部分为0 或 达到尾数所需的精度。只要满足一个就OK,这里也是为什么小数会丢精度的关键所在,在下面的例子会进行讲解。
对于没有精度丢失的转换,结束的条件是:小数部分为 0。
比如,0.25转成二进制
0.25 x 2 = 0.5 0
0.5 x 2 = 1.0 1 小数部分为0,结束
③ 标准化以确定尾数和无偏移指数
根据 IEEE 754标准,需要将二进制小数点放在最左边的 1 之后,比如,100.101需要左移 2位,变成 1.00101x2²,无偏移指数是 2;0.0011需要右移 3位,变成 1.1x2⁻³,无偏移指数是 -3。
这个步骤其实就是 IEEE 754标准的一个硬性规定,解决了上面提到的浮点数漂浮不定的问题。
④ 确认偏移指数
偏移指数,即用无偏移指数(步骤3 产生的结果)加上固定的偏移值127(2⁸-1=127),再转换成二进制。
假如,无偏移指数是 4,那么,偏移指数就等于4+127=131,转换成二进制就是10000011,然后,将二进制结果值10000011填充到 Exponent指数区域。
⑤ 移除尾数的前导 1
在步骤3中,需要将二进制小数点放在最左边的 1 之后,因此,每个小数点前面的值都是固定的 1(也叫做前导 1),在 IEEE 754标准中,会将这个前导 1移除,从而节省了 1个bit,再将结果值填充到二进制的 Significand尾数区域,不足部分填 0。
比如,1.001110011 移除小数点前固定的 1 变成了001110011,然后将结果值001110011填充到 Significand尾数区域。
为了更好的解释上面 5个步骤,这里以十进制19.59375转换成IEEE 754二进制为例进行讲解,整个过程如下图:
(2) 十进制转二进制
这里以十进制转 IEEE754单精度浮点数并且有精度丢失为例,核心流程包含 6个步骤:
① 确认 Sign符号位
比如,19.59375的符号为 0(正数),-1.1的符号为 1(负数),将结果值(0/1)填充到二进制的 Sign bit区域。
② 将十进制转成纯二进制
这个步骤,只是纯粹地将十进制转换成二进制。
- 对于整数部分,通过不断地除以 2,直到商为 0;
- 对于小数部分,采用逐步乘 2取整,直到小数部分为 0或达到所需的精度;
需要注意,对于小数部分处理结束的条件有 2个:小数部分为0 或 达到尾数所需的精度。
对于有精度丢失的转换,结束的条件是:达到所需的精度。
比如,0.3转成二进制
0.3 x 2 = 0.6 0
0.6 x 2 = 1.2 1
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1
0.6 x 2 = 1.2 1 开始进入循环,只能达到所需的精度后按需舍入结束
0.2 x 2 = 0.4 0
③ 标准化以确定尾数和无偏移指数
根据 IEEE 754标准,需要将二进制小数点放在最左边的 1 之后,比如,100.101需要左移 2位,变成 1.00101x2²,无偏移指数是 2;0.0011需要右移 3位,变成 1.1x2⁻³,无偏移指数是 -3。
这个步骤其实就是 IEEE 754标准的一个硬性规定,解决了上面提到的浮点数漂浮不定的问题。
④ 确认偏移指数
偏移指数,即用无偏移指数(步骤3 产生的结果)加上固定的偏移值127(2⁸-1=127),再转换成二进制。
假如,无偏移指数是 4,那么,偏移指数就等于4+127=131,转换成二进制就是10000011,然后,将二进制结果值10000011填充到 Exponent指数区域。
⑤ 移除尾数的前导 1
在步骤3中,需要将二进制小数点放在最左边的 1 之后,因此,每个小数点前面的值都是固定的 1(也叫做前导 1),在 IEEE 754标准中,会将这个前导 1移除,从而节省了 1bit,再将结果值填充到二进制的 Significand尾数区域,不足部分填0。
比如,1.001110011 移除小数点前固定的 1 变成了001110011,然后将结果值001110011填充到 Significand尾数区域。
⑥ 按需向上或者向下舍入
在步骤2中,小数部分转换成二进制的时候不是因为乘 2使得小数部分为 0结束,而是因为产生了循环,导致达到了尾数所需的精度(单精度 23bit,双精度 52bit),对于超出的精度范围,需要如何处理?
答案:按需向上或者向下舍入
为了更好的解释上面 6个步骤,这里以十进制-123.3转换成 IEEE 754二进制为例进行讲解,整体流程如下图:
注意:截图中步骤6黄色字体1001,是指超出尾数 23bit范围的二进制数,需要被舍入
通过上面两个例子的分析,相信大家还是会有困惑:为什么是二进制中存储的是偏移指数而不是指针?为什么偏移指数是通过指数加上一个固定的偏移值?这个固定的偏移值是怎么计算的?为什么尾数需要把前导 1移除?IEEE 754 的舍入规则是什么?下面我们就一一解答。
(3) 二进制转十进制
为了帮助大家更好地理解 IEEE 754的转换,这里还提供了 2个 IEEE 754单精度浮点数二进制转十进制的逆向例子,如下图:
(4) 偏移指数
偏移指数,也叫指数偏移值(exponent bias),即浮点数表示法中指数域的编码值,等于指数的实际值加上某个固定的偏移值,IEEE 754标准规定该固定值偏移为2 ͤ⁻¹-1,其中 e为存储指数的 bit长度,因此,单精度浮点数的固定偏移值是2⁸⁻¹-1=127,双精度浮点数的固定偏移值是2¹¹⁻¹-1=1023。
(5) 为什么需要偏移指数?
从 1.00101 x 2² 和 1.1 x 2⁻³ 可以看出来,指数有正负数的区分,即有符号的区分,因此,IEEE 754标准中的偏移指数主要解决两个问题:
- 表示负指数:在使用二进制表示浮点数的指数时,如果采用纯粹的二进制表示,那么需要额外的符号位来表示指数的正负。采用偏移指数的方式,可以将指数全部看作非负数,因为将偏移量添加到指数部分后,所有的指数都是正数,0则表示了最小的指数。
- 排序浮点数:使用偏移指数的方式可以更容易地对浮点数进行排序。因此第 1点已经把指数全部转换成了无符号,所以,浮点数的比较直接变成了对二进制的自然排序比较,不需要单独处理符号位和指数部分的符号。
(6) 固定值偏移值如何计算?
① 单精度浮点数
对于单精度浮点数,它的指数域是 8个bit,表示的有符号范围是-127~128(-2⁷-1 ~ 2⁷),如何让这个范围 >=0 ?
答案:加上 127。所以,IEEE 754标准把 127设定为单精度浮点数的固定偏移值。
② 双精度浮点数
同理,对于双精度浮点数,它的指数域是 11个bit,表示的有符号范围是-1023~1024(-2¹⁰-1 ~ 2¹⁰),如何让这个范围 >=0 ?
答案:加上 1023。所以,IEEE 754标准把 1023设定为双精度浮点数的固定偏移值。
具体信息如下图:
(7) 为什么要移除尾数的前导 1?
在讲解浮点数构成时提过,浮点数的小数点是浮动的,因此,IEEE 754标准定义了一套固定的格式:在二进制数中,通过移位,将小数点前面的值固定为 1,IEEE754 称这种形式的浮点数为规范化浮点数。
因此,对于规范化浮点数,既然尾数的前导永远是 1,那干脆不存储,尾数其实比实际的多 1位,也就是说单精度的是 24位,双精度是 52位。
(8) IEEE 754 的舍入规则
关于舍入,IEEE 754标准提供了 4种方法:
① 舍入到最接近
这是 IEEE 754标准的默认方式,将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。
取偶数最关键的步骤是找到一个中间值,先确定要保留的有效数字,找到要保留的有效数字最低位的下一位。如果这位是进制的一半,而且之后的位数都为 0,则这个值就是中间值。
这里以二进制为例,有效位数保留到小数点后 2位:
- 10.00011,中间值为 10.00100,小于中间值,向下舍入为 10.00
- 10.00110,中间值为 10.00100,大于中间值,向上舍入为 10.01
- 10.11100,中间值为 10.11100,等于中间值,要保留的最低有效位 1 为奇数,向上舍入为 11.00
- 10.10100,中间值为 10.10100,等于中间值,要保留的最低有效位 0 为偶数,向下舍入为 10.10
因此,上述十进制-123.3转换成 IEEE 754二进制例子的舍入方式,采用向上舍入,即最后一位 +1。
② 朝 +∞方向舍入
3个要点:
- 正数多余位不全为 0,进位1
- 正数多余位全为 0,直接截尾
- 负数直接截尾
这里以二进制为例,有效位数保留到小数点后 3位:
- 0.0011001,正数,从小数点后 4位起,不全为0,则向上进位(最后一位 +1),结果值为 1.010,
- 0.0010000,正数,从小数点后 4位起,全为0,则直接截尾(从 4位起全部舍弃),结果值为 0.001
- -0.0011010,负数直接截尾(从 4位起全部舍弃),结果值为 -0.001
③ 朝 -∞方向舍入
3个要点:
- 正数直接截尾
- 负数多余位全为0,直接截尾
- 负数多余位不全为 0,进位1
这里以二进制为例,有效位数保留到小数点后 3位:
- 0.0011001,正数,则直接截尾(从 4位起全部舍弃),结果值为 0.001
- -0.001000,负数,从小数点后 4位起全为 0,则直接截尾(从 4位起全部舍弃),结果值 -0.001
- -0.001101,负数,从小数点后 4位起不全为 0,则向上进位(最后一位 +1),结果值-0.010
④ 朝 0方向舍入
2个要点:
- 正数直接截尾
- 负数直接截尾
数学上有 4舍5入,计算机中 0舍1入,因此,朝 0方向舍入就是直接舍弃。
这里以二进制为例,有效位数保留到小数点后 3位:
- 0.001100,正数,则直接截尾(从 4位起全部舍弃)弃,结果值 0.001
- -0.001100,负数,则直接截尾(从 4位起全部舍弃)弃,结果值 -0.001
四、非规范浮点数
上文提到了规范化浮点数,既然有规范化浮点数,是不是也存在非规范化浮点数?非规范化浮点数又是什么呢?
在 IEEE 754标准中,将“指数部分全是0,尾数部分非0”这样的浮点数称为非规范化浮点数,一般用于表示 0或者无限接近 0的很小的数字。
另外,IEEE 754标准还规定:非规范化浮点数的指数偏移值比规范化浮点数的指数偏移值小 1。
例如,最小的规范化单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规范化的单精度浮点数的指数域编码值为0,对应的指数实际值也是 -126 而不是-127。实际上非规范化浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规范化浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规范化浮点数的尾数小于1且大于0。
下图展示了非规范化单精度浮点数的二进制表示:
五、特殊值
另外,IEEE 754中还定义4个特殊值,如下表:
- 正负无穷大:指数全是 1,尾数全是 0,代表这个数是正负无穷大(±∞),正负取决于 S符号位。
- NaN:指数全是 1,尾数非 0,代表这不是一个数字(NaN,Not a Number)。
- 0:指数全是 0,尾数全是0,代表这个数是±0,正负取决于 S符号位。
- 很小的值:指数全是 0,尾数不全是0,代表这个数是一个很小的非规范化浮点数。
六、浮点数的比较和运算
有了 IEEE 754标准,浮点数的比较就很简单,基本上可以按照符号位、指数域、尾数域的顺序作字典比较。显然,所有正数大于负数;正负号相同时,指数的二进制表示法更大的其浮点数值更大。
浮点数的运算和函数包含以下几种:
- 加减乘除(Add、subtract、multiply、divide)。在加减运算中负零与零相等:−0.0=0.0
- 平方根(Square root):
- 浮点余数。返回值 x-(round(x/y)*y)。
- 近似到最近的整数 𝑟 𝑜 𝑢 𝑛 𝑑(𝑥)。如果恰好在两个相邻整数之间,则近似到偶数。
- 比较运算. -Inf <负的规范化浮点数数<负的非规范化浮点数< -0.0 = 0.0 <正的非规范化浮点数<正的规范化浮点数< Inf;
- 特殊比较:-Inf = -Inf, Inf = Inf, NaN与任何浮点数(包括自身)的比较结果都为假,即 (NaN ≠ x) = false.
七、为什么 1.0-0.9 != 0.1?
先看 Java中的一个例子:
通过运行结果可以发现:在 Java中,不管是单精度 float,还是双精度 double,1.0 - 0.9 != 0.1,为什么?
从上面的讲解,我们已经知道浮点数 IEEE 754标准在内存中的存储方式,这里我们再简单的分析1.0 - 0.9场景:
1.0 可以精确转换成IEEE 754标准二进制为:0 01111111 00000000000000000000000
0.9 转换成IEEE 754标准二进制为:
符号位:0
偏移指数:-1 + 127 = (126)₁₀ = (0111 1110)₂
尾数:
0.9 x 2 = 1.8 1
0.8 x 2 = 1.6 1
0.6 x 2 = 1.2 1
0.2 x 2 = 0.4 0
0.4 x 2 = 0.8 0
0.8 x 2 = 1.6 1 开始进入循环,只能达到所需的精度后按需舍入结束
0.6 x 2 = 1.2 1
在0.9 转换成 IEEE 754标准的二进制时,出现了循环,这样的话,不管是单精度还是双精度,0.9转换成二进制之后都有精度损失,所以,对于 float 或者 double浮点数 0.9,转换后其真实值已经不是 0.9,因此,1.0 - 0.9 != 0.1。
当然,除了这个例子,还有很多经典的例子,比如 w s m 为什么等于 0.30000000000000004?
八、总结
本文讲解了定点数和浮点数,重点分析了浮点数以及IEEE 754标准下浮点数是如何存储的。
- 浮点数一般用科学计数法表示
- IEEE 754标准的浮点数二进制实际包含:符号位,偏移指数,尾数 3部分。
- 十进制小数在转换为二进制时,如果可以精确转换,则不存在精度丢失。
- 十进制小数在转换为二进制时,如果无法精确转换(存在循环或者超出尾数的范围),则存在精度丢失的问题。
- IEEE 754舍入规则有 4种方法:舍入到最接近、朝 +∞方向舍入、朝 -∞方向舍入以及朝 0方向舍入。