信息的表示与存储
-
通常情况下,程序将内存视为一个非常大的数组,数组的元素是由一个个的字节组成,每个字节(八个二进制位)都由一个唯一的数字来表示,我们称为地址(address),这些所有的地址的集合就称为虚拟地址空间(virtual address space)。指针指向的便是虚拟地址空间,但其具体实现颇为复杂。
-
一个字节是由8个位(bit)组成,在二进制表示法中,每一个位的值可能有两种状态:0或者1。当这8个位全为0时,表示一个字节的最小值;当这8个位全为1时,表示最大值。十进制下一个字节取值0-255,十六进制下取值0x00-0xff之间。
计算机常见的进制及其运算
-
基本概念:
- 数码:一个数制中表示基本数值大小的不同数字符号。例如,八进制有8个数码:0、1、2、3、4、5、6、7。
- 基数:一个数值所使用数码的个数。例如,八进制的基数为8,十进制的基数为10。
- 位权:一个数值中某一位上的1所表示数值的大小。例如,八进制的123,1的位权是64,2的位权是8,3的位权是1。
- 按权展开,指将数字按照位权展开相乘,结果转换为十进制就是其真实值,如 0 x 7 f 7 f = 7 × 1 6 3 + 15 × 1 6 2 + 7 × 16 + 15 0x7f7f=7\times16^3+15\times16^2+7\times16+15 0x7f7f=7×163+15×162+7×16+15
-
二进制,逢二进一,数字中只有 0 和 1
-
十进制,逢十进一,数字中含有 0,1,2,3,4,5,6,7,8,9
-
十六进制,逢十六进一,表示形式比较特殊,强制规定如下:10 用 A 表示、11 用 B 表示、12 用 C 表示、13 用 D 表示、14 用 F 表示。
字长
计算机字长是指CPU一次能并行处理的二进制位数,计算机在存储、传送或操作时,作为一个单元的一组二进制码称为字,一个字中的二进制位的位数称为字长。字长总是8的整数倍。
常见字长:1
二进制的位运算与逻辑运算
- 布尔计算
设P和Q:- 非(~):P为1时,~P为0;反之为1
- 与(&):P和Q都为1时,P&Q为1
- 或(|):P和Q任意一个为1,P|Q为1
- 异或(^):P为1Q为0或P为0Q为1时,P^Q为1
- 我们将上述布尔运算扩展至位向量,
- 位向量就是用一些二进制位组成的向量。在很多的情况下,我们可以用一个二进制表示一个对象。但是,我们不能直接用一个变量名表示一个位(单独一个位组成的数据类型是不存在的),于是我们可以考虑将多个位组成基本的数据类型,然后通过对这个基本的数据类型进行操作,从而达到对位进行操作的目的。同时,位了方便,把由位组成的基本数据类型组成数组,这样就可以对一定范围的位数据集合进行操作了。
- 假定位向量A= [ a 1 , a 2 , a 3 , . . . , a n ] [a_1,a_2,a_3, ... ,a_n] [a1,a2,a3,...,an],B= [ b 1 , b 2 , b 3 , . . . , b n ] [b_1,b_2,b_3, ... ,b_n] [b1,b2,b3,...,bn],其逐位运算的结果为位向量W= [ w 1 , w 2 , w 3 , . . . , w n ] [w_1,w_2,w_3, ... ,w_n] [w1,w2,w3,...,wn],这种计算方式称为位运算。
- 例题
2
- 逻辑运算
逻辑代数中,非零为TRUE,0为FALSE,返回0或1分别表示FALSE和TRUE;
逻辑运算与布尔代数运算规则近似,但功能完全不同,举例说明:- 非(!)所有1变为0,0(数)变为1:!54 结果为0,!0结果为1
- 与(&&):12 || 0 结果为0
- 或(||):0 || 8 结果为1
- 逻辑运算的顺序问题:第一个参数求值就可以知道表达式的结果,就不会对第二个参数求值,如a&&5/a中,若a为否(a==0)则不会求5/a,避免被0除;p&&(*p++)不会导致引用空指针。
- 例题:
- 在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中的最小地址。计算机在内存中存放数据的顺序都是从低地址到高地址。
例如,假设一个类型为int的变量x的地址为0x100,即&x的值为0x100。那么x的4个字节将被存储在
存储器的0x100,0x101,0x102和0x103的位置。( 当然这是大端法的写法)
- 字节序即为多字节对象存储在内存中的字节顺序,有两种不同的存储方案:大端法(低位置存低有效字节)和小端法(低位置存高有效字节),两者没有技术上的优劣之分。现代的处理器大多为双端法,大小端都支持,可以配置称大端法或者小端法。以下以0x01234567计算
- 以下有一C语言代码可判断操作系统的大端/小端
#include<stdio.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
union x{
int16_t a;//两个字节
char b;//一个字节
}w;//之所以这么写是因为共用体所有成员的存放顺序是从(内存中)的低地址开始的
w.a=0x01;//
if(w.b==0)
printf("该机器使用大端法\n");
else if(w.b==1)
printf("该机器使用小端法\n");
return 0;
}
另外有一方案可以展示在内存中其真实的存储方式:
#include<stdio.h>
typedef unsigned char *bytePointer;
void showBytes(bytePointer p, size_t length){
size_t i;
for(i=0;i<length;i++){
printf("%.2x ", p[i]);//整数用至少两位的十六进制输出
}
printf("\n");
}
void printInt(int i){
showBytes((bytePointer)&i, sizeof(int));
}
int main(void){
int x=12345;
printInt(x);
return 0;
}
移位运算
-
移位运算是将数值向左向右移动,对于十进制来说就是实现放大十倍和缩小十倍的效果,而对于二进制而言就是放大两倍和缩小两倍的效果。
-
使用源码和补码的数据结构才可以移位(原因详见浮点数表达的详解)
-
左移符号 < < << <<,右移符号 > > >> >>。
-
对于逻辑移位,就是不考虑符号位,移位的结果只是数据所有的位数进行移位。根据移位操作的目的,左移时,低位补0,右移时,高位补0,这适用于使用源码的数据类型如C系语言的unsigned int。
-
算术是带有符号的数据,所以我们不能直接移动所有的位数。根据补码原理(见下),右移为补k个最高有效位之数值。
算术左移和逻辑左移事实上相同。
-
在C语言标准中没有定义应该使用算术右移还是逻辑右移,一般的编译器将有符号数算术右移而将无符号数逻辑右移。Java等语言可以区分算术和逻辑右移,即 > > >> >>为算术右移而 > > > >>> >>>为逻辑右移。
-
右移的逻辑可总结为:
逻辑右移
左端补0,算数右移
左端补最高有效位(事实上一般补符号位)。下图可更好展现这一规则:
整数表示与运算
补码原理
假定整数的二进制有w位,将位向量写作 x ⃗ = [ x w − 1 , x w − 2 , … , x 1 , x 0 ] \vec{x}=[x_{w-1},x_{w-2},\ldots ,x_1,x_0] x=[xw−1,xw−2,…,x1,x0],则有了其无符号表示,即源码,或者可以表示为 B 2 U w ( x ⃗ ) = ∑ i = 0 w − 1 x i 2 i B2U_{w}(\vec{x})=\sum_{i=0}^{w-1}x_i2^i B2Uw(x)=∑i=0w−1xi2i (B2U=“Binary to Unsigned”,即(纯)二进制到无符号数)。其表示长度为w位的正整数,视作映射: B 2 U w : { 0 , 1 } → { 0 , … , 2 w − 1 } B2U_w:\{0,1\}\rightarrow\{0,\ldots,2^{w}-1\} B2Uw:{0,1}→{0,…,2w−1},这个映射是双射,反之也成立。
由此我们可以将最高位赋予负权(negative weight),成为符号位,其位权为 − 2 w − 1 -2^{w-1} −2w−1。则补码后的有符号数字可理解为 B 2 T w ( x ⃗ ) = − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i B2T_w(\vec{x})=-x_{w-1}2^{w-1}+\sum_{i=0}^{w-2}x_i2^i B2Tw(x)=−xw−12w−1+∑i=0w−2xi2i,即为补码数字。
重要数字的源码、补码对比:3
PS:其它类型的码
反码
![](https://i-blog.csdnimg.cn/blog_migrate/d15712750a5886eef8f1f0ebb8af9fa3.png#pic_center)
最高位为 2 w − 1 − 1 2^{w-1}-1 2w−1−1即1表示正,0表示负数,负数的反码是在其原码的基础上符号位不变,其余各个位取反。
移码
本质操作:源码减去偏置量 2 w − 1 − 1 2^{w-1}-1 2w−1−1或者 2 w − 1 2^{w-1} 2w−1
看起来:补码数符号位取反,1表示正,0表示负数。
三种码的取值范围
注意移码有两种方法,一种
C语言的整型类型
![20200905170906792.png](https://i-blog.csdnimg.cn/blog_migrate/5f9d3512d87e4eab673dfe57c14ec7bb.png)
![20200905170901176.png](https://i-blog.csdnimg.cn/blog_migrate/fa4932f803531019a2cf7cf0f92347c3.png)
有无符号数之间转换
在有符号数与无符号数之间进行强制类型转换的结果是保持位值不变,只改变解释位的方式。
-
补码 x 转无符号数
-
x ≥ 0 x\ge0 x≥0,值不变
-
x < 0 x < 0 x<0,转换后的值为 2 w + x 2^w + x 2w+x
-
通式: T 2 U w ( x ) = x w − 1 2 w + x T2U_w(x)=x_{w-1}2^w+x T2Uw(x)=xw−12w+x
-
举例:
-
无符号数 u 转补码
-
u < 2 w − 1 u< 2^{w-1} u<2w−1,值不变
-
u ≥ 2 w − 1 u \ge 2^{w-1} u≥2w−1,转换后的值为 u − 2 w u - 2^w u−2w
-
通式: U 2 T w ( u ) = − u w − 1 2 w + u U2T_w(u)=-u_{w-1}2^w+u U2Tw(u)=−uw−12w+u
-
图示:
-
C语言中的强制类型转换:
- 使用(类型)转换和使用%u输出时转换都是相同的二进制码分别用补码和源码解释;
- 执行一个运算时,出现无符号数会将所有有符号数隐式转换为无符号的,并导致负数-无符号正整数>正数这种吊诡情况。
- 注意函数形参是无符号还是有符号,有符号传入无符号默认无符号,可能会导致负数变成巨大整数。
- size_t是指一个字长位数的无符号数
整数加减法
截断运算
-
对一个 w 位的数字截断为一个 k 位数字,将丢弃高 w − k w-k w−k 位
-
即为 B 2 U w ( [ x w − 1 , x w − 2 , … , x 1 , x 0 ] ) m o d 2 k = ∑ i = 0 w − 1 x i 2 i m o d 2 k = ∑ i = 0 k − 1 x i 2 i B2U_w([x_{w-1},x_{w-2},\ldots,x_1,x_0])\mod 2^k=\sum_{i=0}^{w-1}x_i2^i \mod 2^k=\sum_{i=0}^{k-1}x_i2^i B2Uw([xw−1,xw−2,…,x1,x0])mod2k=i=0∑w−1xi2imod2k=i=0∑k−1xi2i。
-
对于无符号数而言,截断后的数字实际上等于 w m o d 2 k w \mod 2^k wmod2k,即取余。
-
对于有符号数而言, B 2 T k [ x k − 1 , x k − 2 , … , x 1 , x 0 ] = U 2 T k ( B 2 U w ( [ x w − 1 , x w − 2 , … , x 1 , x 0 ] ) m o d 2 k ) B2T_k[x_{k-1},x_{k-2},\ldots,x_1,x_0]=U2T_k(B2U_w([x_{w-1},x_{w-2},\ldots,x_1,x_0])\mod 2^k) B2Tk[xk−1,xk−2,…,x1,x0]=U2Tk(B2Uw([xw−1,xw−2,…,x1,x0])mod2k),注意负数的模数,正方向为正,负方向为负。
无符号加减法
x
+
w
u
y
=
{
x
+
y
,
x
+
y
<
2
w
,
“正常”
x
+
y
−
2
w
,
2
w
≤
x
+
y
<
2
w
+
1
,
“溢出”
x+_w^u y= \begin{cases} x+y ,& x+y<2^w,\text{“正常”}\\ x+y-2^w ,& 2^w \le x+y<2^{w+1},\text{“溢出” } \end{cases}
x+wuy={x+y,x+y−2w,x+y<2w,“正常”2w≤x+y<2w+1,“溢出”
-
解释:
- 当 x + y < 2 w x+y<2^w x+y<2w,和的w+1位值为0,可舍去;
- 当 2 w ≤ x + y < 2 w + 1 2^w \le x+y<2^{w+1} 2w≤x+y<2w+1,最高位(第w+1位,明显超出了字长)等于1,截断它相当于 − 2 w -2^w −2w。
- + w u +_w^u +wu的解释:无符号加法,精度w位。
-
关于溢出:
-
判断是否溢出
0 ≤ x , y ≤ U M A X w s ≐ x + w u y s < x ∣ ∣ s < y ⇒ 发生了溢出 0\le x,y\le UMAX_w\\ s\doteq x +_w^u y\\ s<x||s<y\Rightarrow 发生了溢出 0≤x,y≤UMAXws≐x+wuys<x∣∣s<y⇒发生了溢出 -
模数加法形成了一种数学结构称之为阿贝尔群。其特征是可交换和可结合的,有单位元0和加法逆元(即“减法”)
-
无符号数取反:
− w u x = { x , x = 0 2 w − x , x > 0 其中 0 ≤ x < 2 w -_w^u x= \begin{cases} x,& x=0\\ 2^w-x,& x>0 \end{cases} \\\text{其中}0\le x<2^w −wux={x,2w−x,x=0x>0其中0≤x<2w
解释: − w u x -_w^u x −wux为“无符号逆元”, − w u x + w u x = 0 -_w^u x+_w^u x=0 −wux+wux=0,上式其实就是无符号数转换为其w位补码负数的过程.
故因此,无符号数的减法就是补码的减法。
补码加减法
x
+
w
u
y
=
{
x
+
y
−
2
w
,
2
w
−
1
≤
x
+
y
,
正溢出
x
+
y
,
−
2
w
−
1
≤
x
+
y
<
2
w
−
1
,
正常
x
+
y
+
2
w
,
x
+
y
<
−
2
w
−
1
,
左溢出
x +_w^u y= \begin{cases} x+y-2^w,& 2^{w-1}\le x+y,\text{正溢出}\\ x+y,& -2^{w-1}\le x+y<2^{w-1},\text{正常}\\ x+y+2^{w},& x+y<-2^{w-1},\text{左溢出} \end{cases}
x+wuy=⎩
⎨
⎧x+y−2w,x+y,x+y+2w,2w−1≤x+y,正溢出−2w−1≤x+y<2w−1,正常x+y<−2w−1,左溢出
-
上式可以理解为:
x + w t y = U 2 T w ( T 2 U w ( x ) + T 2 U w ( y ) ) = U 2 T w [ ( x w − 1 2 w + x + y w − 1 2 w + y ) m o d 2 w ] = U 2 T w [ ( x + y ) m o d 2 w ] \begin{aligned} x+_w^t y &=U2T_w(T2U_w(x)+T2U_w(y))\\ &=U2T_w[(x_{w-1}2^w+x+y_{w-1}2^w+y)\mod2^w]\\ &=U2T_w[(x+y)\mod2^w] \end{aligned} x+wty=U2Tw(T2Uw(x)+T2Uw(y))=U2Tw[(xw−12w+x+yw−12w+y)mod2w]=U2Tw[(x+y)mod2w]
也即将源码相加(后取 2 w 2^w 2w的模避免越界,这一步也是溢出的来源)后转补码。 -
溢出的检测:
-
补码的非,也即补码取负数的方式,是将无符号取逆元后的值补码化。
-
当 x = TMin,-x = TMin;当 x ≠ TMin,-x = -x
补码非的位级表示: 对每一位求补(按位取反),结果再加 1(加1的原因是由于补码原理,负数区比正数区大1,正数转负数需要补上,而负数转正数需要加回来避免“超额减去”)
计算补码非的第二种方法:假设 k 是最右边的 1 的位置,对 k 左边的所有位取反
-
整数乘除法
无符号乘法
范围在 0 ≤ x , y ≤ 2 w − 1 0\le x,y\le2^w-1 0≤x,y≤2w−1内的整数x和y可以被表示为w位的无符号数,但是它们的乘法取值范围为 2 w − 1 ⋅ 2 2 w − 2 ≤ x ≤ 2 2 w − 2 2^{w-1}\cdot 2^{2w-2} \le x \le 2^{2w-2} 2w−1⋅22w−2≤x≤22w−2,显然不能按w个字长保存。故我们需要将算出来的数取低的w位,即为 x ⋅ w u y = ( x ⋅ y ) m o d 2 w x \cdot_w^u y=(x \cdot y) \mod 2^w x⋅wuy=(x⋅y)mod2w
有符号乘法
- C语言中的有符号乘法是通过将2w位的乘积截断为w位来实现的。我们将这个数值表示为
x
∗
w
t
y
x \ast_w^t y
x∗wty 。将一个补码数截断为w位相当于先计算该值模
2
w
2^w
2w,再把无符号数转换为补码,即为:
x ∗ w t y = U 2 T w [ ( x ∗ y ) m o d 2 w ] x\ast_w^t y=U2T_w[(x*y) \mod 2^w] x∗wty=U2Tw[(x∗y)mod2w]
需要注意的是,在中间转换过程中,无符号乘法需要在数前面补0以填补至最高位2w-1位,而补码乘法需要将符号扩展(即为将最高位值用于填补)(其实是算术右移的原理),不过 m o d 2 w \mod 2^w mod2w后二进制位级是一致的。 - 补码乘法和无符号乘法在位级上完全等价,推导如下:
- 补码乘法的示例:
乘以常数的优化
由上可知正常的二进制乘法相当慢,因此对于乘以常数应当有特殊的处理办法以尽可能提速。
- 乘以2的幂:
x
∗
2
k
=
x
<
<
k
x \ast 2^k =x<<k
x∗2k=x<<k
证明:
位向量 x ⃗ = [ x w − 1 , x w − 2 , … , x 0 ] B 2 U w ( x ) = ∑ i = 0 w − 1 x i 2 i B 2 U w + k ( x ′ ) = x w − 1 ∗ 2 w − 1 + k + x w − 2 ∗ 2 w − 2 + k + … + x 0 ∗ 2 k = 2 k ∗ B 2 U w ( x ) \text{位向量}\vec{x}=[x_{w-1},x_{w-2}, \ldots,x_0]\\ B2U_w(x)=\sum_{i=0}^{w-1}x_i2^i \\ \begin{aligned} B2U_{w+k}(x') &=x_{w-1}*2^{w-1+k}+x_{w-2}*2^{w-2+k}+\ldots+x_0*2^k\\ &=2^k*B2U_w(x)\\ \end{aligned} 位向量x=[xw−1,xw−2,…,x0]B2Uw(x)=i=0∑w−1xi2iB2Uw+k(x′)=xw−1∗2w−1+k+xw−2∗2w−2+k+…+x0∗2k=2k∗B2Uw(x)
我们可以用移位+加减法的方式计算所有乘以常数:
x ∗ 14 = x ∗ ( 2 4 − 2 ) = ( x < < 4 ) − x ( < < 1 ) x*14=x*(2^4-2)=(x<<4)-x(<<1) x∗14=x∗(24−2)=(x<<4)−x(<<1)
除以2的幂
除法使用右移k位代表除以 2 k 2^k 2k来解决除以常数的问题。证明依照乘法的原理同理易证,需要注意以下几点:
- 无符号数使用逻辑右移而补码数(有符号数)使用算术右移
- 整数模式总是向0的方向舍入,即为整数位的绝对值小的方向
例如:对于x≥0 和 y>0,结果会是 ⌊ x / y ⌋ \left\lfloor x/y \right\rfloor ⌊x/y⌋,而对于x<0和y>0,结果会是 ⌈ x / y ⌉ \left\lceil x/y \right\rceil ⌈x/y⌉。 - 右移偏置量修正:对于补码负数(正数不需要)而言,由于算术右移回导致过度右取,需在右移前加上
(
1
<
<
k
)
−
1
(1<<k)-1
(1<<k)−1以修正。
例如:-12340(补码1100 1111 1100 1100)算术右移4位,偏置值15即为1111,原数+偏置值=1100 1111 1101 1011,算术右移后为1111 1100 1111 1101,即为-771。
浮点数的表示与运算
浮点数的现代标准:IEEE754(最新版本:IEEE754-2019)
- 知乎上的一个汉语解读:https://zhuanlan.zhihu.com/p/480834719
- CSDN上基于IEEE-2008的解读:IEEE754详解
定点小数的表示与运算
本质上,二进制小数只是将位权转换为二进制的产物,即假如存在小数位向量
[
x
w
−
1
,
x
w
−
2
,
…
,
x
0
.
x
−
1
,
x
−
2
,
…
,
x
−
k
]
[x_{w-1},x_{w-2},\ldots,x_0 . x_{-1},x_{-2}, \ldots,x_{-k}]
[xw−1,xw−2,…,x0.x−1,x−2,…,x−k],则必定有
b
=
∑
i
=
−
n
w
2
i
x
i
b=\sum_{i=-n}^w2^ix_i
b=∑i=−nw2ixi为二进制小数的值。
十进制小数和二进制小数互相转换
解释:
-
二进制转十进制采用按权展开的机制,即将上述二进制小数的定义式计算方式与结果转为10进制而已。
-
十进制小数转换成二进制小数采用" 乘2取整,顺序排列"法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。
然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。 -
转换示例:
常见的二进制小数(纯小数)
浮点数的表示
由上可知,定点小数需要实打实地占用位数存储小数位,另外也难以准确显示非2的整数幂的小数,由于计算机字长的限制,当需要表示的数据有很大的数值范围时,它们不能直接用定点小数或者定点整数表示。
IEEE标准中浮点数的类型
IEEE754标准提供了如何在计算机内存中,以二进制的方式存储十进制浮点数的具体标准。其限定浮点数有单精度(32位字长)和双精度(64位字长)。
浮点数的结构
见上图,浮点数分为三个部分:符号位、阶码(指数位)、尾数。
设符号位的值为s(称之为符号),指数位代表的值为E(称之为阶码),尾数位代表的值为M4(称之为尾码),浮点数的表达为
F
=
(
−
1
)
s
∗
M
∗
2
E
F=(-1)^s*M*2^E
F=(−1)s∗M∗2E
类似科学计数法的结构。5
- 符号位:这个很好解释,与补码中的符号位含义相同。0为正区,1为负。
- 尾数:M是二进制纯小数,其值为0~
1
−
ϵ
1-\epsilon
1−ϵ或者0~
2
−
ϵ
2-\epsilon
2−ϵ(???),尾数具体表达的尾码值依赖于阶码是否为零(!!!)
尾数是小数,其位数n+1决定了浮点数的精度,如果尾数采用小数且位数n足够长,则当浮点数运算需要对尾数运算结果舍入时,造成的数据精度损失会比较小。 - 阶码:尾数加权 2 E 2^E 2E,E即为阶码。指数位负责编码阶码,方法是指数位在偏置之后根据偏置后的值编码阶码。
浮点数的实现
根据阶码的类型,浮点数可以分为规格化的值、非规格化的值、 inf \inf inf 或者写作 ∞ \infty ∞、NaN。
以下讨论中,设指数位总位数k,字面值(将区域数视为无符号整数的值)为e,阶码值为E;小数字段总位数w,字面值f,尾数的值为M,浮点数总值F。
规格化
当指数位不全为0且不全为1,字面值 e ≠ 0 & & e ≠ 2 k − 1 e\neq 0 \&\& e\neq 2^k-1 e=0&&e=2k−1则为规格化数。
- 小数位f被解释为描述小数值, 0 ≤ f < 1 0\le f<1 0≤f<1,二进制小数点在小数位的最左边, M = 1 + f M=1+f M=1+f,即“隐含的1开头”6
- 阶码需要偏置,即 E = e − B i a s E=e-Bias E=e−Bias,偏置值( B i a s Bias Bias)为等于 2 k − 1 − 1 2^{k-1}-1 2k−1−1的值,对于单精度为 2 7 − 1 = 127 2^7-1=127 27−1=127,对于双精度为 2 10 − 1 = 1023 2^{10}-1=1023 210−1=1023,所以指数的取值范围为 − ( 2 k − 1 − 2 ) ≤ E ≤ 2 k − 1 − 1 -(2^{k-1}-2) \le E \le 2^{k-1}-1 −(2k−1−2)≤E≤2k−1−1,如单精度为 − 126 ≤ e ≤ 127 -126 \le e \le 127 −126≤e≤127,双精度为 1022 ≤ e ≤ 1023 1022 \le e \le 1023 1022≤e≤1023,这样的话就可以让e始终为无符号二进制数而表示负数指数7
- 因此,规格化数的表示法为 F = ( − 1 ) s ( 1 + f ) 2 e − B i a s F=(-1)^s(1+f)2^{e-Bias} F=(−1)s(1+f)2e−Bias
非规格化
- 当指数位全为0时,阶码必定全为0,则该浮点数为“非规格化”的。
- 非规格化数是为表示小于1的数(尤其是逐渐靠近0的数)和0设计的,
- 阶码值为
1
−
B
i
a
s
1-Bias
1−Bias8,其表示法为
F
=
(
−
1
)
s
f
∗
2
1
−
B
i
a
s
F=(-1)^sf*2^{1-Bias}
F=(−1)sf∗21−Bias
特别地,0的表达为1/0 00000000 0…0,正0和负0有各自的用途,此处不赘述。
∞ \infty ∞和NaN
- 当指数位全为1时,即 e = 2 k − 1 e=2^k-1 e=2k−1时为特殊数。
- 当计算结果的极限趋于无穷大或者超过浮点数的表示范围时,使用0 11111111 0…0,记作
inf
或者
∞
\inf 或者 \infty
inf或者∞;
- 溢出的运算
- 正无穷、负无穷
- 尾数位不为零时表示NaN,“Not a Number”
- 表示虚数、非法计算等情况。
浮点数的取值范围和精确度
规格化数和非规格化数的上下限
以下以32位浮点即单精度显示
类型 | 指数位 | 阶码值 | 小数位 | 总值 |
---|---|---|---|---|
最小的非规格化正数 | 0000 0000 | -126 | 0000 0000 0000 0000 001 | 2 − 126 ∗ 2 − 23 = 2 − 149 ≐ 1.401298464324817071 e − 45 2^{-126}*2^{-23}= 2^{-149} \doteq 1.401298464324817071e-45 2−126∗2−23=2−149≐1.401298464324817071e−45 |
中间的非规格化正数 | 0000 0000 | -126 | 1000 0000 0000 0000 000 | 2 − 126 ∗ 1 / 2 = 2 − 127 2^{-126}*1/2=2^{-127} 2−126∗1/2=2−127 |
最大的非规格化正数 | 0000 0000 | -126 | 1111 1111 1111 1111 111 | 2 − 126 ∗ ( 1 − 2 − 23 ) ≐ 1.8 e − 38 2^{-126}*(1-2^{-23}) \doteq 1.8e-38 2−126∗(1−2−23)≐1.8e−38 |
最小的规格化正数 | 0000 0001 | -126 | 0000 0000 0000 0000 000 | 2 − 126 ≐ 1.175494350822287508 e − 38 2^{-126} \doteq 1.175494350822287508e-38 2−126≐1.175494350822287508e−38 |
1 | 0111 1111 | 0( 2 7 − 1 - ( 2 7 − 1 ) 2^7-1-(2^7-1) 27−1-(27−1)) | 0000 0000 0000 0000 000 | 1 |
最大的规格化正数 | 1111 1110 | 127( 2 8 − 2 − ( 2 7 − 1 ) 2^8-2-(2^7-1) 28−2−(27−1)) | 1111 1111 1111 1111 111 | 2 127 ∗ ( 1 + 1 − 2 − 23 ) = 3.4 e 38 2^{127}*(1+1-2^{-23})=3.4e38 2127∗(1+1−2−23)=3.4e38 |
inf \inf inf | 1111 1111 | 127 | 0000 0000 0000 0000 000 | - |
NaN | 1111 1111 | 127 | - | - |
9 |
- 思考:浮点数的分布是怎样的?
- 浮点数编码的特点:
- 不连续的、不完全的
- 由于表示非完全二进制的小数(如0.2)需要无限循环小数,表达这类数的精度小于2的幂的小数,这类数实际展现时是近似到蓝点上。
- 越靠近0精度越高、越靠近无穷大精度越小,把非规格数与规格数放在一起审视, ieee754浮点数表盘上的蓝点依旧是越靠近0越密集, 越靠近∞越稀疏
- 浮点数的实际有效精度远小于其可表达位数、实际可用范围小于取值范围。关于浮点数真实有效精度的讨论见下。
- 浮点数编码的特点:
浮点数的真实精度和真实可用范围
结论:
- 32位浮点数的精度是7位十进制数
- 32位浮点数的可用范围随精度变化而变化,以下为维基百科上总结的间隔表:
可以看出,追求7位有效十进制数精度需要 0 ≤ ∣ F ∣ ≤ 8 0 \le |F| \le 8 0≤∣F∣≤8,并且事实上不便于存储过小的小数;追求4位有效十进制数精度需要 0 ≤ F ≤ 4096 0 \le F \le 4096 0≤F≤4096,大于16777215甚至无法标识整数。因此,float的适用范围其实相当狭窄,大部分64位应用都尽可能用double。
原因:
余谈:
- 浮点数值转换网站:https://www.h-schmidt.net/FloatConverter/IEEE754.html
浮点数的运算
浮点数的舍入
任何有效数上的运算结果,通常都存放在较长的寄存器中,当结果被放回浮点格式时,必须将多出来的位数丢弃。 有多种方法可以用来舍入浮点数,实际上IEEE标准列出4种不同的方法:
- 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。例如,假如精度为1,2.4向2舍入,3.4向3舍入,但2.5向2舍入,3.5向4舍入。
- 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
- 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
- 朝0方向舍入:会将结果朝0的方向舍入。
浮点数的加法
设浮点数 f 1 = ( − 1 ) s 1 ∗ 2 E 1 ∗ M 1 f_1=(-1)^{s_1}*2^{E_1}*M_1 f1=(−1)s1∗2E1∗M1,浮点数 f 2 = ( − 1 ) s 2 ∗ 2 E 2 ∗ M 2 f_2=(-1)^{s_2}*2^{E_2}*M_2 f2=(−1)s2∗2E2∗M2,浮点计算的规则如下:
- 对阶。
- ① 所谓对阶是指将两个进行运算的浮点数的阶码对齐的操作。对于浮点数来说,两浮点数进行加减,首先看两数的阶码是否相同,即小数点位置是否对齐。
- ② 对阶的具体方法是:首先求出两浮点数阶码的差,即 ⊿ E = E 1 − E 2 ⊿ E = E _1 − E _2 ⊿E=E1−E2 ,将小阶码加上 ⊿ E ⊿ E ⊿E ,使之与大阶码相等,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。
- ③ 对阶的原则是小阶对大阶
- 对阶之后将尾码相加减,即为 M 1 ± M 2 M_1 \pm M_2 M1±M2
- 规格化:包含两个层面
- 浮点数整体变为规格化的数,即为尾数必须为1.x的形式
- 方法:尾数左/右移同时阶码除以/乘以2^(移动位数)直至形成真正的IEEE浮点数形式。
浮点数乘除法
f
1
∗
f
2
=
(
−
1
)
s
1
2
E
1
M
1
∗
(
−
1
)
s
2
2
E
2
M
2
=
(
−
1
)
s
1
⋏
s
2
2
E
1
+
E
2
(
M
1
∗
M
2
)
=
(
−
1
)
s
2
E
M
\begin{aligned} f_1*f_2 &=(-1)^{s_1}2^{E_1}M_1*(-1)^{s_2}2^{E_2}M_2 \\ &=(-1)^{s_1\curlywedge s_2}2^{E_1+E_2}(M_1*M_2)\\ &=(-1)^s2^EM \end{aligned}
f1∗f2=(−1)s12E1M1∗(−1)s22E2M2=(−1)s1⋏s22E1+E2(M1∗M2)=(−1)s2EM
注意:
- 符号位是由s1和s2的异或决定,毕竟负负得正
- 修正:
- 如M ≥ 2, 将M右移(1位), E加1
- 如 E 超出范围,则溢出
- 将M舍入,以符合小数部分的精度要求
- M的乘法参照无符号整数乘法进行
浮点数运算的性质
需要注意的点:浮点数的计算不完全是阿贝尔群
- a. 浮点数的计算是封闭的,但是在溢出时会直接变为inf,虚数等非法计算会变成NaN
- b. 浮点数的计算是可交换的,但并非可结合的
例子: 3.14 + ( l e 20 − l e 20 ) = 3.14 3.14+(le20-le20)=3.14 3.14+(le20−le20)=3.14, 3.14 + l e 20 − l e 20 = ∞ 3.14+le20-le20=\infty 3.14+le20−le20=∞
( 1 e 20 ∗ 1 e 20 ) ∗ 1 e − 20 = inf (1e20*1e20)*1e-20= \inf (1e20∗1e20)∗1e−20=inf, 1 e 20 ∗ ( 1 e 20 ∗ 1 e − 20 ) = 1 e 20 1e20*(1e20*1e-20)= 1e20 1e20∗(1e20∗1e−20)=1e20
原因:溢出和舍入的不确定性
另外乘法对加法不存在分配性,如 1 e 20 ∗ ( 1 e 20 − 1 e 20 ) = 0.0 1e20*(1e20-1e20)=0.0 1e20∗(1e20−1e20)=0.0, 1 e 20 ∗ 1 e 20 − 1 e 20 ∗ 1 e 20 = ∞ 1e20*1e20-1e20*1e20=\infty 1e20∗1e20−1e20∗1e20=∞,原因是溢出和舍入不精确 - c.由于舍入性质,即使在较小、较低有效位数的情况下也可能产生错误,尤其是在采用低字长的浮点表示法时
- d.每个元素都有逆元,除了inf和NaN。
- e.符合单调性
著名案例:爱国者1的精度问题:
该表代表数据结构的字长,也即在编译为CPU汇编指令后的实际使用的指令长度,由此在32位系统下编译的软件可在64位系统下使用,因其最高实际占用32位字长,反之不能。 ↩︎
四个计算过程:
↩︎
需要注意几点:1. 补码的范围并非完全对称, ∣ T m i n ∣ = ∣ T m a x ∣ + 1 |T_{min}|=|T_{max}|+1 ∣Tmin∣=∣Tmax∣+1,Tmin没有对应之正数; 2. 最大的无符号数值 U m a x = 2 T m a x + 1 U_{max}=2T_{max}+1 Umax=2Tmax+1; 3. 0在补码中有正和负两种表示。 ↩︎
之所以是“代表的值”,请看后面的“具体实现”,浮点数的设计特点和优点也正体现在对阶码和层码的巧妙处理上。 ↩︎
图中所示的浮点数为单精度浮点的 1.25 ∗ 2 − 3 1.25*2^{-3} 1.25∗2−3即 0.15625 0.15625 0.15625。需要注意的是,由于偏置的存在,只有最高位为1时指数阶码值才可能为正数。 ↩︎
如何理解?以科学计数法方式实现小数,一般小数点前一位且不为0,在“规范化”情况下,我们可以用2的幂表示,因而不需要将小数点前的1展示出来,这样节省了一个二进制位数,还将重点移动到小数位,能够为表示“非规范化”的0和0.x提供格式。 ↩︎
目的是让“非规格”数和“规格化”数的阶码相等,然后让非规格化数的尾数均匀分布在0和最小规格化数的尾数之间,而实现平滑过渡 ↩︎