浮点数在计算机中是如何存储的?

背景

最近在梳理MySQL数据类型相关的知识,遇到了float和double的存储范围和,精度,存储的问题,觉得很有必要弄清楚了浮点数的计算机中是如何存储的。

基础知识

十进制整数转二进制

十进制整数转换为二进制整数采用除2取余,逆序排列法。具体做法是:

  • 用2整除十进制整数,可以得到一个商和余数;
  • 再用2去除商,又会得到一个商和余数,如此进行,直到商为0时为止
  • 然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。

例如 5 的二进制表示为:101

5 / 2 => 商2 余 1
2 / 2 => 商1 余 0
1 / 2 => 商0 余 1

二进制转十进制整数

从右向左用二进制数的每个位上数去乘以2的相应次方,并将所有结果相加。例如5的二进制是:101

1 * 2^0 = 1
0 * 2^1 = 0
1 * 2^2 = 4

相加就等于5

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

什么是二进制的小数? 就是形如101.11数字,注意,这是二进制的,数字只能是0和1。

101.11就等于 1 * 2^2 + 0 *2^1 + 1*2^0 + 1*2^-1 + 1*2^-2 = 4+0+1+1/2+1/4 = 5.75

下面的图展示了一个二进制小数的表达形式。

从图中可以看到,对于二进制小数,小数点右边能表达的值是 1/2, 1/4, 1/8, 1/16, 1/32, 1/64, 1/128 … 1/(2^n)

计算机存储十进制小数时需要先将其转为二进制小数,具体的转换方式是:

  • 整数部分采用十进制转二进制方法进行
  • 小数部分乘以2,然后取整数部分。不断重复该操作直到小数部分为0,或达到指定的精度。

举个例子:1.8125 转为 二进制小数

整数部分为1 转为二进制为 1
0.8125 x 2 1.625 取 1
0.625 x 2 1.25 取 1
0.25 x 2 0.5 取 0
0.5 x 2 1.0 取 1

最终1.8125的二进制是1.1101

但问题在于,不是所有的小数都能转换有限位数的二进制小数。例如10进制0.2的2进制:

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
0.4 x 2 0.8 0
0.8 x 2 1.6 1
0.6 x 2 1.2 1
……

发现了吗?它是乘不尽的,是无限循环(0011)的……

在计算机中,浮点数没有办法精确表示的根本原因在于计算机有限的内存无法表示无限的小数位。只能截断,截断就造精度的缺失。

0.2 的二进制小数表示可以是:

0.2 = 0.00110011

转为十进制为:1/8 + 1/16 + 1/128 + 1/256 = 0.19921875

已经很接近了,如果需要更精确的表示,只需要保留更长的有效位数。这也是双精度的double比单精度的float更精确的原因。

那计算机究竟是如何保存二进制小数的呢?这个就不得不提 IEEE 754 规范了。

IEEE754 规范

详情参考:IEEE_754

浮点数存储

IEEE754规范定义的浮点数存储如下图所示:

一个二进制浮点数存储分为三个部分:

  • 符号位S
  • 指数位部分
  • 尾数位部分

举个例子:十进制浮点数1.8125的二进制形式为1.1101 用二进制科学计数法可以表示为1.1101 * 2^0

那么可以推出任意一个二进制浮点数V可以表示成下面的形式:

(1)(-1)^S表示符号位,当s=0,V为正数;当s=1,V为负数。
(2)M表示有效数字,大于等于1,小于2。
(3)2^E表示指数位。

举例来说,十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出S=0,M=1.01,E=2

十进制的-5.0,写成二进制是-101.0,相当于-1.01×2^2。那么,S=1,M=1.01,E=2

32/64位浮点数

IEEE 754规定,对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M

IEEE 754对指数E和有效数字M还有一些特别规定:

指数偏移值

指数偏移值(exponent bias),是指浮点数表示法中的指数域的编码值为指数的实际值加上某个固定的值,IEEE 754标准规定该固定值为2^{e-1}-1,其中的e为存储指数的比特位的长度(float为8,double为11)。
以单精度浮点数为例,它的指数域是8个比特,固定偏移值是 2^{8-1}-1=128-1=127。单精度浮点数的指数部分实际取值是从-127到128(8位有符号二进制的取值范围)。例如指数实际值为17,在单精度浮点数中的指数域编码值为144,即127 + 17
采用指数的实际值加上固定的偏移值的办法表示浮点数的指数,好处是可以用长度为e个比特的无符号整数来表示所有的指数取值,这使得两个浮点数的指数大小的比较更为容易,实际上可以按照字典序比较两个浮点表示的大小。

规约形式的浮点数

如果浮点数中指数部分的编码值在0<exponent<2^e-2之间,且尾数部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。”规约”是指用唯一确定的浮点形式去表示一个值。

由于这种表示下的尾数有一位隐含的二进制有效数字,为了与二进制科学计数法的尾数(mantissa)相区别,IEEE754称之为有效数(significant)。

特殊值

这里有三个特殊值需要指出:

  • 如果指数是0并且尾数的小数部分是0,这个数是±0(和符号位相关)
  • 如果指数 = 2^e-1并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)
  • 如果指数 = 2^e-1并且尾数的小数部分非0,这个数表示为不是一个数(NaN)

以上规则,总结如下:

形式 指数 小数部分
0 0
非规约形式 0 非0
规约形式 1 - 2^e-2 任意
无穷 2^e - 1 0
NaN 2^e - 1 非零

32位单精度

32位浮点数取值范围

为什么32位规约浮点数的最小的非零数为: $ \underline+ 2^{-126} \approx 1.18 * 10^{-38} $

原因是:真实的指数E的最小值只能是-126,如果是-127,再加上偏移量127就等于零了。而指数域为0表示正负0.0。 $ 2^{-126} $ = $ \frac {1} {2^{126}} \approx 1.1754943508222875 e^{-38} $

为什么32位规约浮点数的最大值的非零数为:$ \underline+(2 - 2^{-23})2^{127} \approx 3.410^{38} $

$ \underline+2^{-1022} \approx 2.22*10^{-308}$

$ \underline+(2 - 2^{-52})2^{1023} \approx 1.7910^{308} $

为什么0x00000009还原成浮点数,就成了0.000000?

首先,将0x00000009拆分,得到第一位符号位s=0,后面8位的指数E=00000000,最后23位的有效数字M=000 0000 0000 0000 0000 1001。
由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:

$ V=(-1)^0 × 0.00000000000000000001001 × 2^{-126} = 1.001 × 2^{-146} $

显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?

首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。
那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。
所以,写成二进制形式,应该是s+E+M,即0 10000010 001 0000 0000 0000 0000 0000。这个32位的二进制数,还原成十进制,正是1091567616。

参考资料

IEEE 754
浅谈C/C++的浮点数在内存中的存储方式
浮点数的二进制表示
十进制转二进制
浮点数为什么不精确

文章目录
  1. 1. 背景
  2. 2. 基础知识
    1. 2.1. 十进制整数转二进制
    2. 2.2. 二进制转十进制整数
    3. 2.3. 十进制小数转换为二进制小数
  3. 3. IEEE754 规范
  4. 4. 浮点数存储
    1. 4.1. 32/64位浮点数
    2. 4.2. 32位单精度
  5. 5. 为什么0x00000009还原成浮点数,就成了0.000000?
  6. 6. 请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?
  7. 7. 参考资料
|