现实世界中不仅有整数,还有小数,有没有一种编码,能同时表示它们呢。IEEE 754 是 20 世纪 80 年代以来最广泛使用的浮点数运算标准,为许多 CPU 与浮点运算器所采用,简单来说,它采用了类似科学记数法的形式来表示整数和小数。该标准的设计者,William Morton Kahan 教授在 1989 年获得图灵奖。
产生背景
因为 定点数 存在两个缺陷
- 不能有效的表示非常大的数字。
- 例如,表达式 \( 5\times2^{100}\) 是 \((101)_2\) 后面跟着 100 个零的位模式来表示,如果用定点表示法,容易产生溢出。
- 不能有效的表示小数。
为了解决上述问题,浮点数应运而生。
表示方法
科学记数法
参照科学记数法,IEEE 浮点标准采用如下形式来表示一个数(Value)
$$V = (-1)^s \times M \times 2^E$$
- \((-1)^s\) 表示符号位,当 s=0, V 为正数;当 s=1,V 为负数。
- M 表示有效数字,范围属于 [1,2)。
- \(2^E\) 表示指数位。
例 1
十进制的 5.0 写成二进制是 101.0,相当于 \((1.01)\times2^2\)。按照上面的格式,可以得出 s=0,M=1.01,E=2。
有效数字 M
因为 \(1 \le M < 2\),也就是说,M 的整数部分总是为 1,所以我们可以只保留它的小数(fraction)部分,而将开头的 1 省略。这样一来,就能多表示一位有效数字。
- 在单精度浮点格式中,s、exp、fraction 字段分别为 1、8、23 位。
- 在双精度浮点格式中,s、exp、fraction 字段分别为 1、11、52 位。
指数 E
指数 E 的取值情况比较复杂,我们以 float 浮点类型为例。
- 当 exp 既不全为 0,也不全为 1 时
- E = exp - 127。\(这里的;127;是一个偏置值;=;2^7-1\)。 \(有效数字;M=1+fraction。\)
- 当 exp 全为 0 时
- E = 1 - 127。\(有效数字;M=fraction,;不用在前面加上;1\)。特别的,当 fraction 全为 0 时,表示 \(\pm0\) (取决于符号位)。
- 当 exp 全为 1 时
- 如果 fraction 全为 0,表示 \(\pm\infty\) (取决于符号位)。
- 如果 fraction 不全为 0,表示 NaN (Not a Number)。
精度问题
十进制和二进制相互转化
先看看十进制数和二进制数如何相互转换。用下标表示数的基,\(d_{10}\) 表示十进制数,\(d_2\) 表示二进制数。
一个具有 n+1 位整数,m 位小数的十进制数 \(d_{10}\) 表示为
例 2
$$ (1234.5678)_{10} = 1\times10^3 + 2\times10^2 + 3\times10^1 + 4\times10^0 + 5\times10^{-1} + 6\times10^{-2} + 7\times10^{-3} + 8\times10^{-4}$$
同理,一个具有 n+1 位整数 m 位小数的二进制数 \(b_2\) 表示为
例 3
$$ (1010.1001)_2 = 1\times2^3 + 0\times2^2 + 1\times2^1 + 0\times2^0 + 1\times2^{-1} + 0\times2^{-2} + 0\times2^{-3} + 1\times2^{-4} $$
将二进制数转换成十进制数,比较容易,如例 3 所示。而将十进制数转换成二进制数,需要把整数部分和小数部分分开转换,整数部分用 2 除,取余数;小数部分用 2 乘,取整数位。
例 3
把 \((13.125)_{10}\) 转换成二进制数。
整数部分: \( (13)_{10} = (1101)_2 \)
\((1);13\div2=6 ; 余数为;1 \Rightarrow 1 \)
\((2);6\div2=3 ; 余数为;0 \Rightarrow 01 \)
\((3);3\div2=1 ; 余数为;1 \Rightarrow 101 \)
\((4);最后得到的商小于;2,;所以不用再继续除了。 \Rightarrow 1101 \)
小数部分: \( (0.125)_{10} = (001)_2 \)
\((1);0.125\times2=0.25 ;整数位是;0 \Rightarrow .0 \)
\((2);0.25\times2=0.5 ;整数位是;0 \Rightarrow .00 \)
\((3);0.5\times2=1.0 ;整数位是;1 \Rightarrow .001 \)
\((4);最后得到的乘积是个整数,所以不用再继续乘了。\)
所以 \( (13.125)_{10} = (1101.001)_2 \)。
一个十进制数能否用二进制浮点数精确表示,关键在于小数部分。 我们来看一个最简单的小数 \((0.1)_{10}\) 能否精确表示。
\((1);0.1\times2=0.2;整数位是;0 \Rightarrow .0\)
\((2);0.2\times2=0.4;整数位是;0 \Rightarrow .00\)
\((3);0.4\times2=0.8;整数位是;0 \Rightarrow .000\)
\((4);0.8\times2=1.6;整数位是;1 \Rightarrow .0001\)
\((5);0.6\times2=1.2;整数位是;1 \Rightarrow .00011\)
\((6);0.2\times2=0.4;整数位是;0 \Rightarrow .000110 \ 由第(2)步开始循环。\)
…
我们得到一个无限循环的二进制小数 \((0.1)_{10}=(0.0;0011;0011;0011;…)_2\)。
用有限位无法表示无限循环小数,因此,\((0.1)_{10}\) 无法用 IEEE 754 浮点数精确表示。同时我们还可以得出
\((0.2)_{10} = (0.0011;0011 …)_2\)
\((0.4)_{10} = (0.0110;0110 …)_2\)
\((0.6)_{10} = (0.1001;1001 …)_2\)
\((0.8)_{10} = (0.1100;1100 …)_2\)
这四个数也无法精确表示。同理
\((0.3)_{10}\times2 = 0.6 \)
\((0.7)_{10}\times2 = 1.4 \)
\((0.9)_{10}\times2 = 1.8 \)
这三个数也无法精确表示。 所以,从 0.1 到 0.9 这 9 个小数中,只有 0.5 可以精确表示
\((0.5)_{10} = (0.1)_2\)
二进制小数能精确表示的十进制小数的基本规律
一个十进制小数要能用浮点数精确表示,最后一位必须是 5,因为 1 除以 2 永远是 0.5。当然,这是必要条件,并非充分条件。
一个 m 位二进制小数能够精确表示的小数有多少个呢?
当然是 \(2^m\) 个。而 m 位十进制小数有 \(10^{m}\)个,因此,能精确表示的十进制小数的比例是 \(\frac{2^m}{10^{m}}=(0.2)^{m}\)。m 越大,比例值越小。
其实不管是用十进制、二进制或者其它进制,都存在着有限位数无法精确表示的数字。这时候就需要进行舍入。
舍入
IEEE 标准列出 4 种不同的舍入方法
- 最近舍入
- 舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式)。
- 朝 \(+\infty\) 方向舍入
- 朝 \(-\infty\) 方向舍入
- 朝 0 方向舍入
代码验证
show-byte.c
1 | #include<stdio.h> |
little-big-endian
1 | #include <stdio.h> |
我的电脑是 小端序,最低位先输出。
\((1.1)_{10}\)
\((3f8ccccd)_{16} = (0011;1111;1000;1100;1100;1100;1100;1101)_2\)
- 符号位 S = 0
- 指数 E = exp-127 = 127-127 = 0
- 小数部分 = \(000;\overline{1100}\)
- 有效数字 M = 1.000 1100 1100 1100 1100 1101
- 舍入 \(11\Rightarrow1\)
- \(Value = (-1)^0\times(1.000;1100;…;1101)\times2^0 = 1.000;1100;1100;1100;1100;1101\)
\((3ff99999999999a)_{16} = (0011;1111;1111;0001;1001;1001;…;1001;1010)_2\)
- 符号位 S = 0
- 指数位 E = exp-1023 = 1023-1023 = 0
- 小数部分 = \(0001;\overline{1001}\)
- 有效数字 M = 1.0001 1001 1001 1001 … 1010
- 舍入 \(10\Rightarrow1\)
- \(Value = (-1)^0\times(1.0001;1001;…;1010)\times2^0 = 1.0001;1001;1001;…;1001;1010\)
\((1.3)_{10}\)
\((3fa66666)_{16} = (0011;1111;1010;0110;0110;0110;0110;0110)_2\)
- 小数部分 = \(010;\overline{0110}\)
- 舍入 \(01\Rightarrow0\)
\((3ff4cccccccccccd)_{16} = (0011;1111;1111;0100;1100;1100;…;1100;1101)_2\)
- 小数部分 = \(0100;\overline{1100}\)
- 舍入 \(11\Rightarrow1\)
\((1.7)_{10}\)
\((3fd9999a)_{16} = (0011;1111;1101;1001;1001;1001;1001;1001)_2\)
- 小数部分 = \(101;\overline{1001}\)
- 舍入 \(10\Rightarrow1\)
\((3ffb333333333333)_{16} = (0011;1111;1111;1011;0011;0011;…;0011;0011)_2\)
- 小数部分 = \((1011;\overline{0011})\)
- 舍入 \(00\Rightarrow0\)
\((1.9)_{10}\)
\((3ff33333)_{16} = (0011;1111;1111;0011;0011;0011;0011;0011)_2\)
- 小数部分 = \(111;\overline{0011}\)
- 舍入 \(00\Rightarrow0\)
\((3ffe666666666666)_{16} = (0011;1111;1111;1110;0110;0110;…;0110;0110)_2\)
- 小数部分 = \((1110;\overline{0110})\)
- 舍入 \(01\Rightarrow0\)
拓展
Q1
为什么 exp 需要加上一个偏置值?
A1
采用这种方式表示的目的是简化比较。因为,指数的值可能为正也可能为负,如果采用补码表示的话,符号位 S 和 Exp 自身的符号位将导致不能简单的进行大小比较。正因为如此,指数部分通常采用一个无符号的正数值存储。