最近可能会更几篇计算机组成原理相关的文章,主要秋招面试的时候遇到,以及要做的项目里有涉及到的内容,不会特别细致吧,主要还是目前自己的学习记录。
浮点数表示
这里的浮点数表示是基于 IEEE754 标准,IEEE 浮点数标准用 V = ( − 1 ) s × M × 2 E V = (-1)^s \times M \times 2^E V=(−1)s×M×2E 的形式来表示一个数:
- 符号(sign): s s s 决定这数是负数( s = 1 s=1 s=1)还是正数( s = 0 s=0 s=0)
- 阶码(exponent):
E
E
E 浮点数表示是指数,作用是对浮点数甲醛,权重是 2 的
E
E
E 次幂(可能是负数)
k k k 位的阶码字段 e x p = e k − 1 . . . e 1 e 0 exp=e_{k-1}...e_1e_0 exp=ek−1...e1e0 编码阶码 E E E 这里还和 B i a s Bias Bias 有关,放在后面浮点数类型分类细说 - 尾数(significand): M M M 是一个二进制小数, n n n 位小数字段 f r a c = f n − 1 . . . f 1 f 0 frac=f_{n-1}...f_1f_0 frac=fn−1...f1f0 编码尾数 M,这里编码结果依赖阶码是否为 0,具体也放在后面类型分类细说。
C语言的单精度浮点格式中(32位),
s
s
s、
e
x
p
exp
exp 和
f
r
a
c
frac
frac 字段分别为 1 位、
k
=
8
k=8
k=8 位和
n
=
23
n=23
n=23 位,得到一个 32 位的表示,尾数部分 23 位,换算成十进制就是
2
23
=
8388608
2^{23} = 8388608
223=8388608,所以十进制精度只有 6~7 位。
C语言的双精度浮点格式中(64位),
s
s
s、
e
x
p
exp
exp 和
f
r
a
c
frac
frac 字段分别为 1 位、
k
=
11
k=11
k=11 位和
n
=
52
n=52
n=52 位,得到一个 64 位的表示,尾数部分 52 位,换算成十进制就是
2
52
=
4503599627370496
2^{52} = 4503599627370496
252=4503599627370496,所以十进制精度只有 15~16 位。
类型分类
规格化的值
这是最普遍的情况。当
e
x
p
exp
exp 的位模式既不全为 0(数值 0),也不全为 1(单精度数值为 255,双精度数值为 2047)时,都属于这类情况。在这种情况中,阶码字段被解释为以偏置(biased)形式表示的有符号整数。
也就是说,阶码的值是 E = e − B i a s E = e-Bias E=e−Bias,其中 e e e 是无符号数,其位表示为 e k − 1 . . . e 1 e 0 e_{k-1}...e_1e_0 ek−1...e1e0,而 B i a s Bias Bias 是一个等于 2 k − 1 − 1 2^{k-1}-1 2k−1−1(单精度是 127,双精度是 1023)的偏置值。由此产生指数的取值范围,对于单精度是 − 126 + 127 -126~+127 −126 +127,而对于双精度是 − 1022 + 1023 -1022~+1023 −1022 +1023。
小数字段 f r a c frac frac 被解释为描述小数值 f f f,其中 0 ≤ f < 1 0 \le f \lt 1 0≤f<1,其二进制表示为 0. f n − 1 . . . f 1 f 0 0.f_{n-1}...f_1f_0 0.fn−1...f1f0,也就是二进制小数点在最高有效位的左边。尾数定义为 M = 1 + f M=1+f M=1+f。这种方式可以叫做隐含的以 1 开头的(implied leading 1)表示,因为我们可以把 M M M 看成一个二进制表达式为 1. f n − 1 f n − 2 . . . f 0 1.f_{n-1}f_{n-2}...f_0 1.fn−1fn−2...f0 的数字。这种方式可以让我们获得一个额外的精度位,因为我们总是可以通过调整阶码的方式,让尾数以 1 开头,那么就不需要额外再用一位显式地表示它。
非规格化的值
当阶码域为全 0 时,所表示的数是非规格化形式。在这种情况下,阶码值是 E = 1 − B i a s E=1-Bias E=1−Bias,而尾数的值是 M = f M=f M=f 也就是小数字段的值,不包含隐含的开头的 1。
这里阶码值为 1-Bias 而不是简单的 -Bias 可以提供一种从非规格化值平滑转换到规格化值的方法。
非规格化数有两个用途:
-
提供一种表示数值 0 的方法,上面的规格化数,必须总是使 M ≥ 1 M \ge 1 M≥1,因此我们就不能表示 0。+0.0 的浮点表示的位模式为全 0:符号位是 0,阶码字段全为 0(表明是一个非规格化值),而小数域也全为 0,这就得到 M = f = 0 M=f=0 M=f=0。当符号位为 1,而其它域全为 0 时,我们得到值 -0.0。根据 IEEE 的浮点格式,值 +0.0 和 -0.0 在某些方面被认为是不同的,比如 1/+0.0 会得到正无穷大(后面会提到),1/-0.0 会得到负无穷大。
-
表示那些非常接近于 0.0 的数。它们提供了一种属性,称为逐渐溢出(gradual underflow),其中,可能的数值分布均匀地接近于 0.0。这里顺便提一下,浮点数可表示的数并不是均匀分布的,而是越靠近原点处越稠密
特殊值
当阶码全为 1 的时候为特殊值。当小数域全为 0 时,得到的值表示无穷,当 s = 0 s=0 s=0 时是 + ∞ +\infty +∞,或者当 s = 1 s=1 s=1 时是 − ∞ -\infty −∞。当我们把两个非常大的数相乘,或者除以零时,无穷能够表示溢出的结果。
当小数域为非零时,结果值被称为 “NaN”,即 Not a Number 的缩写。一些运算的结果不能是实数或无穷,就会返回这样的 NaN 值,比如当计算 − 1 \sqrt{-1} −1 或 ∞ − ∞ \infty - \infty ∞−∞ 时。在某些应用中,表示未初始化的数据时,它们也很有用处。
数字示例
下面表格展示 8 位浮点格式的非负值示例(
k
=
4
k=4
k=4 的阶码位和
n
=
3
n=3
n=3 的小数位。偏置量是 7)
从上表就可以看出非规格化值与规格化值之间的平滑过渡。
浮点数转换
下面主要讲解是浮点数十进制和二进制之间的相互转换
十进制转换为二进制
可以分为三步:
- 整数部分转换
- 小数部分转换
- 合并结果
用一个例子来说明,比如说 (float)1.6
(1) 整数部分转换,使用除 2 取余法即可。1) 1 % 2 = 1; 2) 1 / 2 = 0 over,得到整数部分的二进制数为 1。
(2) 小数部分转换,使用乘 2 取整法:
---------------
0.6
×
2
=
1.2
0.6 \times 2 =1.2
0.6×2=1.2 => 1
---------------
0.2
×
2
=
0.4
0.2 \times 2 = 0.4
0.2×2=0.4 => 0
---------------
0.4
×
2
=
0.8
0.4 \times 2 = 0.8
0.4×2=0.8 => 0
---------------
0.8
×
2
=
1.6
0.8 \times 2 = 1.6
0.8×2=1.6 = 1
…
最终得到的小数部分的表示为 0.1001 1001 1001 1001 1001 100
(3) 合并结果 1.1001 1001 1001 1001 1001 100 转换为浮点数标准为
1.1...0
×
2
0
1.1...0 \times 2^{0}
1.1...0×20
E
=
01111111
E = 0111 1111
E=01111111
二进制转十进制
其实就是一个逆过程,但需要注意的是,十进制转二进制后,我们能保存的位数是有限的,所以会存在精度丢失,比如上面的 1.6 转换时无限循环的,那么从十进制转换到二进制,再从二进制转换回十进制本身是不可逆的。
浮点运算
浮点运算主要涉及到的就是加减法和乘除法,以及在实际应用中要注意的问题。
加减法
浮点数的加减预算一般由以下五个步骤完成
对阶
对阶就是将两个进行运算的浮点数的阶码对齐。目的是为使两个浮点数的尾数能够进行加减运算。
具体方法是求出两个浮点数阶码的差,即 △ E = E x − E y \triangle E=E_x - E_y △E=Ex−Ey,将小阶码加上 △ E \triangle E △E,使之与大阶码相等,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。
需要注意的几个地方:
- 对阶原则是小阶对大阶,之所以这样做是因为若大阶对小阶,则尾数的数值部分的高位需移出,而小阶对大阶移出的是尾数的数值部分的低位,这样损失的精度更小。
- 若 △ E = 0 \triangle E=0 △E=0,说明两浮点数的阶码已经相同,无需再做对阶操作了。
- 采用补码表示的尾数右移时,符号位保持不变。
- 由于尾数右移时是将最低位移出,会损失一定的精度,为减少误差,可以保留若干移出的位,供以后舍入处理用。
尾数运算
尾数运算就是完成对阶后的尾数相加减。这里和定点数加减运算一样。
结果规格化
为保证浮点数表示的唯一性,浮点数在机器中都是以规格化形式存储的。就是尾数必须是 1.M 的形式。在进行了上面的尾数相加/减运算后,尾数可能是非规格形式,因此需要进行规格化操作。
根据不同情况,规格化操作分为左规操作和右规操作:
- 左规操作:用于类似 M = 0.0101 M=0. 0101 M=0.0101的情况,将尾数左移,同时阶码减去移动次数
- 右规操作:用于类似 M = 10. x x x M=10.xxx M=10.xxx或 M = 11. x x x M=11.xxx M=11.xxx的形式,将尾数右移,同时阶码加上移动次数,需要注意的是,加减操作,最多会需要右移 1 次
舍入处理
由前面的步骤可知,浮点运算在对阶或右规时,尾数需要右移,被右移出去的位会被丢掉,从而造成运算结果精度的损失。为了减少这种精度损失,可以将一定位数的移出位先保留起来,称为保护位,在规格化后用于舍入处理。
IEEE754 标准列出了四种可选的舍入处理方法:
- 就近舍入 (round to nearest) 这是标准列出的默认舍入方式,和四舍五入类似,需要注意就是如果是中间值,会向偶数舍入。例如,对于32位单精度浮点数来说,若超出可保存的23位的多余位大于等于100…01,则多余位的值超过了最低可表示位值的一半,这种情况下,舍入的方法是在尾数的最低有效位上加1;若多余位小于等于011…11,则直接舍去;若多余位为100…00,此时再判断尾数的最低有效位的值,若为0则直接舍去,若为1则再加1。
- 向 + ∞ +\infty +∞舍入,正数多余位不全为0,则向尾数最低有效位进 1;对负数来说,简单舍去。
- 向 − ∞ -\infty −∞舍入,负数多余位不全为0,向尾数最低有效位进 1;对正数来说,简单舍去。
- 向 0 舍入,直接截断舍去。
溢出判断
浮点数的溢出是看最后阶码的值是否产生溢出判断的:
- 若阶码的值超过了阶码所能表示的最大正数,则为上溢,若浮点数为正数为正上溢,记为 + ∞ +\infty +∞,若浮点数为负数,则为负上溢,记为 − ∞ -\infty −∞。
- 若阶码的值超过了阶码所能表示的最小负数,则为下溢,若浮点数为正数,则为正下溢,记为 + 0 +0 +0,若浮点数为负数,则为负下溢,记为 − 0 -0 −0。
具体的计算实例可以看这篇博客的内容,后续有时间补个例子。。。
乘除法
与加减法类似,除了不需要对阶了:
- 阶码相加/减
- 尾数相乘/除
- 结果规格化
- 舍入处理
- 溢出判断
后面有时间补个例子。。。
浮点数使用事宜
- 防止大数吃小数,因为在浮点数加减的过程中,需要对阶,相应会将小数的尾数部分右移,如果大数很大, 那么就会把小数的精度给吃掉。
- 浮点数加法不满足结合律,比如 (3.14+1e10)-1e10得到0.0,而3.14+(1e10-1e10值为3.14,本质上就是上面的大数吃小数问题。
- 浮点数乘法也不满足结合律,比如 (1e20*1e20)*1e-20求值为+100,而1e20*(1e20*1e-20)将得出1e20,本质上浮点数表示是有上限。
- 浮点乘法在加法上不具备分配性,比如 1e20*(1e20-1e20)求值为 0.0,而 1e20*1e20-1e20*1e20会得出 NaN。
参考资料
深入理解计算机系统 —— 2.4 浮点数
浮点数的运算步骤
真有小伙伴不知道浮点数如何转二进制吗?