计算机组成 -- 定点数 + 浮点数
浮点数的不精确性
1 | >>> 0.3 + 0.6 |
- 32bit是无法表达所有实数的,只能表达
2^32
次方不同的数字,差不多40亿 - 40亿个数字比起无限多的实数集合只是沧海一粟,应该让这40亿个数映射到实数集合上的哪些数字,在实际应用中才最划算
定点数
- 用4个bit表示
0~9
的整数,那么32个bit就可以表示8个这样的整数 - 然后把最右边的2个
0~9
的整数,当成小数部分;把最左边6个0~9
的整数,当成整数部分 - 这样用32bit可以表示
0.00~999999.99
这1亿个实数 - 这种用二进制表示十进制的编码方式,称为BCD编码(Binary-Coded Decimal)
- 应用非常广泛:超市、银行
- 缺点
- 浪费
- 本来32bit可以表示40亿个不同的数字,但在BCD编码下,只能表示1亿个数
- 如果精确到分,能够表达的最大金额为100W,非常有限
- 无法同时表示很大的数字和很小的数字
- 浪费
浮点数
- 浮点数的科学计数法的表示,有一个IEEE的标准,定义了两个基本的格式
- 一个是用32bit表示单精度的浮点数,即float
- 一个是用64bit表示双精度的浮点数,即double
- float
- 符号位s – 1bit
- 用来表示正数还是负数,所有浮点数都是有符号的
- 指数位e – 8bit
- 8bit表示的整数空间,为
0~255
,**1~254
映射到-126~127
这254个有正有负的数上(差值为127**) 0
和255
是标志位,主要用于表示0和一些特殊的数
- 8bit表示的整数空间,为
- 有效数位f – 23bit
- $(-1)^s \times 1.f \times 2^e$
- 没办法表示0,需要借助指数位e
- 符号位s – 1bit
格式 | s=符号位 | e=指数位 | f=有效数位 |
---|---|---|---|
float | 1 bit | 8 bit | 23 bit |
s | e | f | 浮点数 | 备注 |
---|---|---|---|---|
0 or 1 | 0 | 0 | 0 | |
0 | 255 | 0 | 无穷大 | |
1 | 255 | 0 | 无穷小 | |
0 or 1 | 255 | != 0 | NAN | |
0 or 1 | 0 | != 0 | 0.f | 暂未消化 |
特殊数的Java实现
1 | /** |
1 | private static String getFloatBinaryString(float f) { |
1 | 0-00000000-00000000000000000000000 -> 0.0 |
表示
0.5
- $0.5 = (-1)^0 \times 1.0 \times 2^{-1}$
- $s=0, f=0, e=-1$
- $e$表示从
-126~127
个,而-1是其中第126个
- 如果不考虑符号,浮点数能够表示的最小的数和最大的数,差不多是**$1.17 \times 10^{-38}$和$3.40 \times 10^{38}$**
- 比BSD编码能表示的范围大很多
- 浮点数是没法精确表示0.3和0.6的,而0.5是能够被精确地表示成二进制浮点数的
- 浮点数无论是表示还是计算(如加法)其实都是近似计算
1 | public static void main(String[] args) { |
1 | 0-01111110-00000000000000000000000 -> 0.5 |
9.1
- 输入一个任意的十进制浮点数,背后都会对应一个二进制表示
- 首先把整数部分转换成一个二进制,9对应为
1001
- 把对应的小数部分也换成二进制
- 二进制小数
0.1001
:**$1 \times 2^{-1} + 0 \times 2^{-2} + 0 \times 2^{-3} + 1 \times 2^{-4} = 0.5625$** - 十进制小数 -> 二进制小数
- × 2,然后看是否超过1,如果超过1,就记为1,并把结果减去1,进一步循环操作
- 参照下表,0.1的二进制小数为
0.000110011...
- 把整数部分和小数部分拼接在一起,9.1的二进制表示为
1001.000110011...
- 二进制的科学计数法:**$1.001000110011… \times 2^{3}$**
- 此时可以对应到浮点数的格式,**$s=0, e=3+127=130=0b10000010, f=00100011001100110011001$**
- f最长只有23位,无限循环二进制小数会被截断(只能用近似值表示的根本原因)
- 指数位有正有负,映射到浮点数格式,需要加上偏移量127
- 浮点数9.1的二进制表示
0-10000010-00100011001100110011001
- 再换算成十进制为
9.09999942779541015625 ≈ 9.1
,只是一个近似值
- 再换算成十进制为
行号 | 算式 | 是否超过1 | 二进制位 | 剩余差值 | 备注 |
---|---|---|---|---|---|
1 | 0.1 × 2 = 0.2 | 否 | 0 | 0.2 | |
2 | 0.2 × 2 = 0.4 | 否 | 0 | 0.4 | |
3 | 0.4 × 2 = 0.8 | 否 | 0 | 0.8 | |
4 | 0.8 × 2 = 1.6 | 是 | 1 | 0.6 | |
5 | 0.6 × 2 = 1.2 | 是 | 1 | 0.2 | |
6 | 0.2 × 2 = 0.4 | 否 | 0 | 0.4 | 开始重复,第2~4行 |
运算(加法)
- 原则:先对齐、再计算
- 两个浮点数的指数位可能是不一样的,需要先把两个指数位变成一样的,然后只去计算有效位的加法
0.5 + 0.125
- 0.5对应的指数位是
-1
,有效位是00...
(前面默认有个1) - 0.125对应的指数位为
-3
,有效位是00...
(前面默认有个1)
- 先对齐,把指数位统一成
-1
,对应的有效位1.00...
要对应的右移两位,变成0.01...
- 计算两者相加的有效位
1.f
,变成了1.01
,而指数位为-1
- 实现这样一个加法,只需要位移,在电路层面,并没有引入太多新的复杂性
符号位s | 指数位e | 有效位1.f | |
---|---|---|---|
0.5 | 0 | -1 | 1.00… |
0.125 | 0 | -3 | 1.00… |
0.125:对齐指数位 + 有效位右移 | 0 | -1 | 0.01… |
0.5+0.125 | 0 | -1 | 1.01… |
1 | public static void main(String[] args) { |
1 | 0-01111110-00000000000000000000000 -> 0.5 |
精度丢失
- 在上面浮点数的加法过程中,指数位较小的数,需要进行有效位的右移,最右侧的有效位就会被丢弃
- 两个相加数的指数位相差得越大,位移的位数就越大,可能丢失的精度就越大
- 32位浮点数的有效位长度一共只有23位,如果两个数的指数位相差超过23位,较小的数右移24位之后,所有的有效位都丢失了
- 解决方案(软件层面算法):Kahan summation algorithm
1 | float a = 1 << 24; |
1 | 0-10010111-00000000000000000000000 -> 1.6777216E7 |
1 | float a = 1 << 24; |
小结
- 一般情况下,在实际应用中,如果需要精确数值(如银行存款、电商交易),一般会使用定点数或者整数
- 浮点数适用情况:不需要非常精确的计算结果
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.