第 2 章 信息的表示和处理
2.1 信息存储
大多数计算机使用 8 8 8 位的块,或者字节,作为最小的可寻址内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址。所有可能地址的集合就称为虚拟地址空间。
2.1.2 字数据大小
每台计算机都有一个字长,指明指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为 w w w 位的机器而言,虚拟地址的范围为 0 ∼ 2 w − 1 0 \sim 2^w-1 0∼2w−1,程序最多访问 2 w 2^w 2w 个字节(因为一个地址对应一个字节)。例如, 32 32 32 位字长机器限制虚拟地址空间为 4 4 4 GB, 64 64 64 位字长机器限制虚拟地址空间为 16 16 16 EB。
C 语言支持整数和浮点数多种数据格式。如 long
一般在
32
32
32 位程序中为
4
4
4 字节,在
64
64
64 位程序中为
8
8
8 字节。为了避免同一数据类型在不同机器上大小不一, ISO C99 引入了大小固定、不随编译器和机器设置变化的数据类型,包括 int32_t
和 int64_t
,它们分别为
4
4
4 字节和
8
8
8 字节。
大部分数据类型都编码为有符号数值,除非有前缀关键字 unsigned
。对关键字的顺序以及包括还是省略可选关键字来说,C 语言允许存在多种形式,如 unsigned long
,unsigned long int
,long unsigned
,long unsigned int
都是同一个意思。
2.1.3 寻址和字节顺序
对于跨越多字节的程序对象(如 int
),必须建立两个规则:这个对象的地址是什么以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所用字节中最小的地址。例如,一个 int
型变量 x
的地址为 0x100
,那么 x
的
4
4
4 个字节将分别被存储在内存的 0x100,0x101,0x102,0x103
的位置。
排列一个对象的多个字节有两个通用的规则:大端法和小端法。某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象(即最低有效字节在最前面的方式),称为小端法;而另一些机器按照从最高有效字节到最低有效字节的顺序存储(最高有效字节在最前面的方式),称为大端法。
假设 int
变量
x
x
x 的地址为 0x100
,十六进制值为 0x01234567
(最高有效字节为 0x01
,最低有效字节为 0x67
),则按照上述两种方式存储的示意图如下:
2.1.5 表示代码
int sum(int x, int y)
{
return x+y;
}
考虑上面 C 函数,在不同机器上编译时,生成如下字节表示的机器代码:
机器类型 | 机器代码 |
---|---|
Linux 32 | 55 89 e5 8b 45 0c 03 45 08 c9 c3 |
Windows | 55 89 e5 8b 45 0c 03 45 08 5d c3 |
Sun | 81 c3 e0 08 90 02 00 09 |
Linux 64 | 55 48 89 e5 89 7d fc 89 75 f8 03 45 fc c9 c3 |
可以发现,不同机器使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。
2.1.8 C 语言中的逻辑运算
C 语言中包含三个逻辑运算符 ||,&&,!
,分别对应命题逻辑中的 OR,AND 和 NOT 运算。逻辑运算认为非
0
0
0 为TRUE,而
0
0
0 表示 FALSE。逻辑运算符有一个重要的特点:如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。例如,表达式 a&&5/a
将不会造成除
0
0
0,而表达式 p&&*p++
也不会造成引用空指针。
2.1.9 C 语言中的移位运算
C 语言提供了左移(<<
)和右移(>>
)两种移位运算。
左移运算比较简单,对于一个
w
w
w 位的变量 x
,x<<k
表示 x
向左移动 k
位,丢弃最高 k
位,并在右端补 k
个
0
0
0。移位量应该是一个在
0
∼
w
−
1
0\sim w-1
0∼w−1 的值。移位运算从左到右可以结合,所以 x<<j<<k
等价于 (x<<j)<<k
。
右移相对更加复杂。一般而言存在两种右移:逻辑右移和算术右移。逻辑右移在左端补 k k k 个 0 0 0,算术右移在左端补 k k k 个最高有效位的值。例如,对二进制序列 [ 01100011 ] 2 [01100011]_2 [01100011]2,逻辑右移和算术右移都得到 [ 00000110 ] 2 [00000110]_2 [00000110]2;而序列 [ 10010101 ] 2 [10010101]_2 [10010101]2,逻辑右移得到 [ 00001001 ] 2 [00001001]_2 [00001001]2,算术右移得到 [ 11111001 ] 2 [11111001]_2 [11111001]2。可见算术右移对于有符号数是非常有用的。
C 语言标准并没有明确定义对于有符号数应该使用哪种类型的右移。然而,实际上几乎所有的编译器/机器组合都对有符号数使用算术右移;而对于无符号数,右移一定是逻辑的。
上述移位已经规定移位量 k k k 在 0 ∼ w − 1 0\sim w-1 0∼w−1 范围内。如果 k ≥ w k\geq w k≥w 会怎么样呢?C 语言标准未定义这样的行为,不过对于大多数机器,实际位移量将变为 k % w k\% w k%w。例如,对于
int
型变量x
,x>>36
相当于x>>4
。这种行为是没有保证的,所以应当尽量避免。
在 C 语言中,加减法的优先级比移位运算符要高。
1<<2+3<<4
等价于1<<(2+3)<<4
而不是(1<<2)+(3<<4)
。
2.2 整数表示
2.2.2 无符号数的编码
将无符号数的编码看成向量,则对于向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x}=[x_{w-1},x_{w-2},\cdots,x_0] x=[xw−1,xw−2,⋯,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。
例如, B 2 U 4 ( [ 0101 ] ) = 0 ∗ 2 3 + 1 ∗ 2 2 + 0 ∗ 2 1 + 1 ∗ 2 0 = 5 B2U_4([0101])=0*2^3+1*2^2+0*2^1+1*2^0=5 B2U4([0101])=0∗23+1∗22+0∗21+1∗20=5。
2.2.3 补码编码
对于许多应用,我们希望能表示负数值。最常见有符号数的表示方式为补码(two’s complement)形式。在这个定义中,将字的最高有效位解释为负权。对于向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯ , x 0 ] \vec{x}=[x_{w-1},x_{w-2},\cdots,x_0] x=[xw−1,xw−2,⋯,x0], 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。
最高有效位 x w − 1 x_{w-1} xw−1 也叫符号位,它的权重为 − 2 w − 1 -2^{w-1} −2w−1。当符号位为 1 1 1 时,表示值为负;当符号位为 0 0 0 时,表示值为非负。例如, B 2 T 4 ( [ 1011 ] ) = − 1 ∗ 2 3 + 0 ∗ 2 2 + 1 ∗ 2 1 + 1 ∗ 2 0 = − 5 B2T_4([1011])=-1*2^3+0*2^2+1*2^1+1*2^0=-5 B2T4([1011])=−1∗23+0∗22+1∗21+1∗20=−5。
不难得到,补码表示的范围为 − 2 w − 1 ∼ 2 w − 1 − 1 -2^{w-1}\sim 2^{w-1}-1 −2w−1∼2w−1−1,即 ∣ T M i n ∣ |TMin| ∣TMin∣(最小值) = ∣ T m a x ∣ =|Tmax| =∣Tmax∣(最大值) + 1 +1 +1。
有符号数的其他表示方法:
反码(Ones’ Complement):最高有效位的权是 − ( 2 w − 1 − 1 -(2^{w-1}-1 −(2w−1−1) 而不是 − 2 w − 1 -2^{w-1} −2w−1,其它和补码一样。 B 2 O w ( x ⃗ ) = − x w − 1 ( 2 w − 1 − 1 ) + ∑ i = 0 w − 2 x i 2 i B2O_w(\vec{x})=-x_{w-1}(2^{w-1}-1)+\sum_{i=0}^{w-2}x_i2^i B2Ow(x)=−xw−1(2w−1−1)+∑i=0w−2xi2i。
原码:最高有效位为符号位,用来确定剩下的位应该取负权还是正权:
B 2 S w ( x ⃗ ) = ( − 1 ) x w − 1 ∗ ( ∑ i = 0 w − 2 x i 2 i ) B2S_w(\vec{x})=(-1)^{x_{w-1}}*(\sum_{i=0}^{w-2}x_i2^i) B2Sw(x)=(−1)xw−1∗(∑i=0w−2xi2i)。
这两种表示方法对 0 0 0 有两种不同编码方式: + 0 +0 +0 和 − 0 -0 −0。把 [ 00 ⋯ 0 ] [00\cdots0] [00⋯0] 解释为 + 0 +0 +0,而 − 0 -0 −0 在原码中为 [ 10 ⋯ 0 ] [10\cdots0] [10⋯0],在反码中为 [ 11 ⋯ 1 ] [11\cdots1] [11⋯1]。术语补码来源于这样一个情况,对于非负数 x x x,我们用 2 w − 1 2^w-1 2w−1 计算 − x -x −x 的 w w w 位表示。术语反码来源于这样一个属性,我们用 [ 111 ⋯ 1 ] − x [111\cdots1]-x [111⋯1]−x 来计算 − x -x −x 的反码表示。
2.2.4 有符号数和无符号数之间的转换
对于 大多数 C 语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式(二进制编码)不变。强制类似转换的结果保持位置不变,只是改变了解释这些位的方式。
2.2.5 C 语言中的有符号数与无符号数
通常,大多数数字都默认为是有符号的。例如,当声明一个像 12345
或者 0x1A2B
这样的常量时,这个值就被认为是有符号的。要创建一个无符号常量,必须加上后缀字符 U
或 u
,例如 12345U
或者 0x1A2Bu
。
当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么 C 语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,再执行运算。可以从以下几个例子体会:
表达式 | 类型 | 求值 |
---|---|---|
0==0u | 无符号 | 1 |
-1<0 | 有符号 | 1 |
-1<0u | 无符号 | 0 |
2147483647>-2147483647-1 | 有符号 | 1 |
2147483647u>-2147483647-1 | 无符号 | 0 |
2147483647>(int)2147483648u | 有符号 | 1 |
-1>-2 | 有符号 | 1 |
(unsigned)-1>-2 | 无符号 | 1 |
上述例子中很小心地将 T M i n 32 TMin_{32} TMin32 写成
-2147483647-1
。这和 C 头文件limits.h
是一致的:#define INT_MAX 2147483647 #define INT_MIN (-INT_MAX - 1)
这是由于补码的不对称性导致的,一般不能简单地写成
0x80000000
2.2.6 扩展一个数字的位表示
要将一个无符号数转换为一个更大的数据类型,我们只要简单地在表示的开头添加 0 0 0,这种运算被称为零扩展;要将一个补码数字转换为一个更大的数据类型,应该在表示中添加最高有效位的值,称为符号扩展。
short sx = -12345; //cf c7
unsigned short usx = sx; //cf c7
int x = sx; //ff ff cf c7
unsigned int = usx; //00 00 cf c7
值得一提的是,从一个数据大小到另一个数据大小的转换,以及无符号数和有符号数之间的转换相对顺序能够影响一个程序的行为。考虑下面代码:
short sx = -12345;
unsigned uy = sx;
printf("%u",uy);
//结果为 4294954951,即uy的编码变为 ff ff cf c7
这表明当把 short
转换成 unsigned
时,我们先要改变大小(扩展位表示),再完成有符号数到无符号数的转换。也就是说 (unsigned) sx
等价于 (unsigned) (int) sx
,求值得到
4294954951
4294954951
4294954951,而不等价于 (unsigned) (unsigned short) sx
,后者求值得到
53191
53191
53191。这个规则是 C 语言标准要求的。
2.2.7 截断数字
当将一个 w w w 位的数截断为一个 k k k 位的数时,只需要丢弃高 w − k w-k w−k 位,剩下编码即为截断后的结果。
int x = 53191; // 00 00 cf c7
short sx = (short) x; // cf c7 -> x为-12345
int y = sx; //ff ff cf c7 -> y为-12345
2.2.8 关于有符号数和无符号数的建议
考虑以下代码:
float sum_elements(float a[], unsigned length)
{
int i;
float result = 0;
for (i=0;i<=length-1;i++)
result += a[i];
return result;
}
乍一看代码貌似没有什么问题,但是当 length=0
时程序本该什么都不计算并返回
0.0
0.0
0.0,但是由于 length
为无符号类型,此时 length-1
将被解释为无符号的 0xffffffff
,相当于写成了 i<= 4,294,967,295
,导致数组越界访问非法内存。
避免这一类错误的一种方法就是绝不使用无符号数。实际上,除了 C 以外很少有语言支持无符号整数。
2.3 整数运算
2.3.1 无符号加法
- 定义参数 x x x 和 y y y 的无符号加法运算为 + w u +_w^u +wu,其中 0 ≤ x , y ≤ 2 w 0\leq x,y\leq 2^w 0≤x,y≤2w:
x + w u y = { x + y − 2 w , 2 w ≤ x + y < 2 w + 1 x + y , x + y < 2 w x+_w^u y=\lbrace_{x+y-2^w,2^w \leq x+y<2^{w+1}}^{x+y,x+y<2^w} x+wuy={x+y−2w,2w≤x+y<2w+1x+y,x+y<2w
例如, x = 9 x=9 x=9 和 y = 12 y=12 y=12 分别为 [ 1001 ] [1001] [1001] 和 [ 1100 ] [1100] [1100]。它们的和为 21 21 21,即 [ 10101 ] [10101] [10101],丢弃溢出位,得最终结果为 [ 0101 ] [0101] [0101],即 5 5 5。
- 检测无符号数加法中的溢出:令 s = x + y s=x+y s=x+y,当且仅当 s < x s<x s<x (或等价的 s < y s<y s<y)时发生了溢出。
模数加法形成了一种数学结构,称为阿贝尔群。也就是说,模数加法是可交换的和可结合的。它有一个单位元 0 0 0,并且每个元素有一个加法逆元,即对于每个值 x x x,必然有某个值 − x -x −x 满足 − x + w u x = 0 -x+_w^ux=0 −x+wux=0。
-
无符号数求反
对于满足 0 ≤ x < 2 w 0\leq x<2^w 0≤x<2w 的任意 x x x,其 w w w 位的无符号逆元 − x -x −x 由下式给出:
− x = { 2 w − x , x > 0 x , x = 0 -x=\lbrace_{2^w-x,x>0}^{x,x=0} −x={2w−x,x>0x,x=0
2.3.2 补码加法
- 对满足 − 2 w − 1 ≤ x , y ≤ 2 w − 1 − 1 -2^{w-1}\leq x,y \leq 2^{w-1}-1 −2w−1≤x,y≤2w−1−1 的整数 x x x 和 y y y,定义补码加法 + w t +_w^t +wt:
x + w t 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^t y=\left\{ \begin{matrix} x+y-2^w,2^{w-1}\leq x+y\qquad 正溢出\\ x+y,-2^{w-1} \leq x+y <2^{w-1} \qquad 正常\\ x+y+2^w,x+y<-2^{w-1}\qquad 负溢出 \end{matrix} \right. x+wty=⎩ ⎨ ⎧x+y−2w,2w−1≤x+y正溢出x+y,−2w−1≤x+y<2w−1正常x+y+2w,x+y<−2w−1负溢出
- 检测补码加法中的溢出
对满足 T M i n w ≤ x , y ≤ T M a x w TMin_w \leq x,y \leq TMax_w TMinw≤x,y≤TMaxw 的 x x x 和 y y y,令 s = x + y s=x+y s=x+y。当且仅当 x > 0 , y > 0 , s ≤ 0 x>0,y>0,s\leq0 x>0,y>0,s≤0 时发生正溢出;当且仅当 x < 0 , y < 0 , s ≥ 0 x<0,y<0,s\geq0 x<0,y<0,s≥0 时发生负溢出。
2.3.3 补码的非
对满足
T
M
i
n
w
≤
x
≤
T
M
a
x
w
TMin_w \leq x \leq TMax_w
TMinw≤x≤TMaxw 的
x
x
x,其补码的非(加法逆元)
−
x
-x
−x 为:
−
x
=
{
T
M
i
n
w
,
x
=
T
M
i
n
w
−
x
.
x
>
T
M
i
n
w
-x=\left\{ \begin{matrix} TMin_w, \qquad x=TMin_w \\ -x. \qquad x>TMin_w \end{matrix} \right.
−x={TMinw,x=TMinw−x.x>TMinw
看下面这个例子.
#include <stdio.h>
int main()
{
int a = 0x80000000;
printf("%d\n", a);
int b = -a;
printf("%d\n", b);
}
最终输出 a
和 b
的值是一致的。
求补码非的位级表示的两种方法
- 对任意整数值
x
,计算表达式-x
和~x+1
得到的结果完全一样。- 假设 k k k 是最右边 1 1 1 的位置,那么对位 k k k 左边的所有位取反。
x ⃗ \vec{x} x ∼ x ⃗ \sim\vec{x} ∼x − x -x −x 5 [0101] -6 [1010] -5 [1011] -8 [1000] 7 [0111] -8 [1000] 7 [0111] -8 [1000] -7 [1001]
2.3.4 无符号乘法
-
无符号乘法
对满足 0 ≤ x , y ≤ U M a x w 0 \leq x,y \leq UMax_w 0≤x,y≤UMaxw 的 x x x 和 y y y 有:
x ∗ w u y = ( x ∗ y ) m o d 2 w x*_w^uy=(x*y) mod 2^w x∗wuy=(x∗y)mod2w
即,将积截断保留低 w w w 位。
2.3.5 补码乘法
和无符号乘法基本一致,即,将积截断保留低 w w w 位,并按照补码形式解释积。
模式 | x x x | y y y | x ∗ y x*y x∗y | 截断后结果 |
---|---|---|---|---|
无符号 | 5 [101] | 3 [011] | 15 [001111] | 7 [111] |
补码 | -3 [101] | 3 [011] | -9 [110111] | -1 [111] |
2.3.6 乘以常数
为了加快乘法的速度,编译器做了一项重要的优化:用移位和加法运算代替乘以常数因子的乘法。
-
乘以 2 2 2 的幂
设 x x x 的位表示为 [ x w − 1 , x w − 2 , ⋯ , x 0 ] [x_{w-1},x_{w-2},\cdots,x_0] [xw−1,xw−2,⋯,x0],则 x ∗ 2 k x*2^k x∗2k 的无符号表示为 [ x w − k − 1 , x w − k − 2 , ⋯ , x 0 , 0 , ⋯ , 0 ] [x_{w-k-1},x_{w-k-2},\cdots,x_0,0,\cdots,0] [xw−k−1,xw−k−2,⋯,x0,0,⋯,0],右边增加了 k k k 个 0 0 0。
-
乘以任意整数的幂
由于整数乘法比位移和加法的代价要大得多,许多 C 语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。例如
x*14
,利用 14 = 2 3 + 2 2 + 2 1 14=2^3+2^2+2^1 14=23+22+21,编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1)
。更好的是,编译器还可以利用 14 = 2 4 − 2 1 14=2^4-2^1 14=24−21,将乘法重写为(x<<4)-(x<<1)
,进一步提升运算速度。
2.3.7 除以 2 的幂
-
除以 2 2 2 的幂的无符号除法
C 变量
x
和k
有无符号数值 x x x 和 k k k,且 0 ≤ k < w 0\leq k < w 0≤k<w,则 C 表达式x>>k
产生数值 ⌊ x / 2 k ⌋ \lfloor x/2^k \rfloor ⌊x/2k⌋(向下取整)。 -
除以 2 2 2 的幂的补码除法,向下舍入
C 变量
x
和k
分别有补码值 x x x 和无符号数值 k k k,且 0 ≤ k < w 0\leq k < w 0≤k<w,则当执行算术移位时 C 表达式x>>k
产生数值 ⌊ x / 2 k ⌋ \lfloor x/2^k \rfloor ⌊x/2k⌋。对于 x ≥ 0 x \geq 0 x≥0,变量
x
最高有效位为 0 0 0,所以效果与逻辑右移是一样的。而对于 x < 0 x<0 x<0,移位导致结果向下舍入。例如, − 771.25 -771.25 −771.25 将向下舍入变为 − 772 -772 −772。所以我们需要调整测量来处理负数 x x x 的除法。 -
除以 2 2 2 的幂的补码除法,向上舍入
C 变量
x
和k
分别有补码值 x x x 和无符号数值 k k k,且 0 ≤ k < w 0\leq k < w 0≤k<w,则当执行算术移位时 C 表达式(x+(1<<k)-1)>>k
产生数值 ⌈ x / 2 k ⌉ \lceil x/2^k \rceil ⌈x/2k⌉。例如, − 771.25 -771.25 −771.25 将向上舍入变成 − 771 -771 −771。
这个分析表明对于使用算术右移的补码机器,C 表达式
(x<0 ? x+(1<<k)-1 : x) >> k
将计算 x / 2 k x/2^k x/2k。通过上述分析可知,除以 2 2 2 的幂可以通过逻辑或者算术右移来实现。这也正是为什么大多数机器上提供这两种类型的位移。不幸的是,这种方法不能推广到除以任意常数。桐乘法不同,我们不能用除以 2 2 2 的幂的除法来表示除以任意常数 K K K 的除法。
2.4 浮点数
浮点表示对形如 V = x × 2 y V=x\times2^y V=x×2y 的有理数进行编码。它对执行设计非常大的数 ( ∣ V ∣ > > 0 |V|>>0 ∣V∣>>0)、非常接近于 0 0 0( ∣ V ∣ < < 1 |V|<<1 ∣V∣<<1)的数,以及更普遍地作为实数运算的近似值的计算,是很有用的。
2.4.1 二进制小数
考虑形如 b m b m − 1 ⋯ b 1 b 0 . b − 1 b − 2 ⋯ b − n + 1 b − n b_mb_{m-1}\cdots b_1b_0.b_{-1}b_{-2}\cdots b_{-n+1}b_{-n} bmbm−1⋯b1b0.b−1b−2⋯b−n+1b−n 的表示法,其中每个二进制数 b i b_i bi 的取值为 0 0 0 或 1 1 1。这种方法表示的数 b b b 定义如下: b = ∑ i = − n m 2 i × b i b=\sum_{i=-n}^{m}2^i\times b_i b=∑i=−nm2i×bi。
例如, 101.1 1 2 101.11_2 101.112 表示数 1 ∗ 2 2 + 0 ∗ 2 1 + 1 ∗ 2 0 + 1 ∗ 2 − 1 + 1 ∗ 2 − 2 = 5.75 1*2^2+0*2^1+1*2^0+1*2^-1+1*2^-2=5.75 1∗22+0∗21+1∗20+1∗2−1+1∗2−2=5.75。
不难发现,小数点左移一位相当于这个数除以 2 2 2,小数点右移一位相当于乘 2 2 2。注意,形如 0.11 ⋯ 1 1 2 0.11\cdots11_2 0.11⋯112 的数表示的是刚好小于 1 1 1 的数。例如, 0.11111 1 2 0.111111_2 0.1111112 表示 63 64 \frac{63}{64} 6463,可以用简单的表达法 1.0 − ϵ 1.0-\epsilon 1.0−ϵ 来表示这样的数。
2.4.2 IEEE 浮点表示
IEEE 浮点标准用 V = ( − 1 ) s × M × 2 E V=(-1)^s\times M \times 2^E V=(−1)s×M×2E 的形式来表示一个数:
- 符号: s s s 决定数的正负,而对于数值 0 0 0 的符号位解释作为特殊情况处理。
- 尾数: M M M 是一个二进制小数,其范围是 1 ∼ 2 − ϵ 1\sim 2-\epsilon 1∼2−ϵ,或者是 0 ∼ 1 − ϵ 0\sim 1-\epsilon 0∼1−ϵ。
- 阶码: E E E 的作用是对浮点数加权,这个权是 2 2 2 的 E E E 次幂。( E E E 可能是负数)
将浮点数的位表示分为三个字段,分别对这些值进行编码:
- 一个单独的符号位 s s s
-
k
k
k 位的阶码字段
exp=
e k − 1 ⋯ e 1 e 0 e_{k-1}\cdots e_1e_0 ek−1⋯e1e0 编码阶码 E E E -
n
n
n 位小数字段
frac=
f n − 1 ⋯ f 1 f 0 f_{n-1}\cdots f_1f_0 fn−1⋯f1f0 编码尾数 M M M,但是编码出来的值依赖于阶码 E E E 是否等于 0 0 0。
下图给出了将这三个字段装进字中两种最常见的格式。在单精度浮点格式(float
)中,s、exp
和 frac
字段分别为
1
1
1 位、
k
=
8
k=8
k=8 位和
n
=
23
n=23
n=23 位,得到一个
32
32
32 位的表示。在双精度浮点格式 (double
) 中,s、exp
和 frac
字段分别为
1
1
1 位,
k
=
11
k=11
k=11 位和
n
=
52
n=52
n=52 位,得到一个
64
64
64 位的表示。
给定位表示,根据 exp
的值,被编码的值可以分成三种不同的情况(最后一种情况有两个变种)。下图说明了对单精度格式的情况。
-
情况 1:规格化的值(
exp
字段既不全为 0 0 0 也不全为 1 1 1)这是最普遍的情况。当
exp
的位模式既不全为 0 0 0 (数值 0 0 0),也不全为 1 1 1(单精度值为 255 255 255,双精度值为 2047 2047 2047)时,都属于这类情况。在这种情况中,阶码字段被解释为以偏置形式表示的有符号整数,也就是说,阶码的值是 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}\cdots e_1 e_0 ek−1⋯e1e0(即阶码字段中存储的位表示), B i a s Bias Bias 是一个等于 2 k − 1 − 1 2^{k-1}-1 2k−1−1(单精度是 127 127 127,双精度是 1023 1023 1023)的偏置值。由此产生指数的取值范围,对于单精度是 − 126 ∼ + 127 -126 \sim +127 −126∼+127,而对于双精度是 − 1022 ∼ + 1023 -1022 \sim +1023 −1022∼+1023。小鼠字段
frac
被解释为描述小数值 f f f,其中 0 ≤ f < 1 0\leq f<1 0≤f<1,其二进制表示为 0. f n − 1 ⋯ f 1 f 0 0.f_{n-1}\cdots f_1f_0 0.fn−1⋯f1f0。尾数 M M M 定义为 1 + f 1+f 1+f。有时,这种方式也叫做隐含的以 1 1 1 开头的表示,因为我们可以把 M M M 看成一个二进制表式为 1. f n − 1 ⋯ f 1 f 0 1.f_{n-1}\cdots f_1f_0 1.fn−1⋯f1f0 的数。既然我们总是能够调整阶码 E E E,使得尾数 M M M 在范围 1 ≤ M < 2 1 \leq M <2 1≤M<2 之中,那么这种表示方法是一种轻松获得一个额外精度位的技巧1。既然第一位总是 1 1 1,那么我们就不需要显示地表示它。 -
情况 2:非规格化的值
当阶码字段全为 0 0 0 时,所表示的数是非规格化形式。在这种情况下,阶码值 E = 1 − B i a s E=1-Bias E=1−Bias(而不是简单的 − B i a s -Bias −Bias),尾数值 M = f M=f M=f,也就是小数字段的值,不包含隐含在开头的 1 1 1。
非规格化数有两个用途。首先,它们提供了一种表示数值 0 0 0 的方法,因为使用规格化数,我们必须总是使 M ≥ 1 M \geq1 M≥1,因此就不能表示 0 0 0。实际上, + 0.0 +0.0 +0.0 的浮点表示的位模式为全 0 0 0:符号位为 0 0 0,阶码字段全为 0 0 0,而小数域也全为 0 0 0,这就得到了 M = f = 0 M=f=0 M=f=0。当符号位为 1 1 1,而其余为 0 0 0,则得到值 − 0.0 -0.0 −0.0。
非规格化值的另一个功能是用于表示那些非常接近于 0.0 0.0 0.0 的数。它们提供了一种称为逐渐下溢的属性,其中,可能的数值分布均匀地接近于 0.0 0.0 0.0。
-
特殊值
当阶码全为 1 1 1 时,所表示的数为特殊值。当
frac
字段全为 0 0 0 时,得到的值表示无穷,当 s = 0 s=0 s=0 时是 + ∞ +\infin +∞,当 s = 1 s=1 s=1 时是 − ∞ -\infin −∞。当我们把两个非常大的数相乘,或者除以 0 0 0 时,无穷能够表示溢出的结果。当
frac
字段不是全为 0 0 0 时,结果值被称为 “NaN”(Not a number)。一些运算结果不能是实数或者无穷时,就会返回这样的 NaN 值,比如 − 1 \sqrt{-1} −1 或者 ∞ − ∞ \infin - \infin ∞−∞。
2.4.3 数字示例
下图展示了 8 8 8 位浮点格式的示例,其中有 k = 4 k=4 k=4 的阶码位和 n = 3 n=3 n=3 的小数位。偏置量 B i a s = 2 3 − 1 = 7 Bias=2^3-1=7 Bias=23−1=7。
观察到最大非规格化数 7 512 \frac{7}{512} 5127 和最小规格化数 8 512 \frac{8}{512} 5128 之间的平滑转变。这种平滑性归功于我们对非规格化数的 E E E 的定义。
下图展示了一些重要的单精度和双精度浮点数的表示和数字值。
下面展示一个将整数值 12345 12345 12345 转换成浮点格式的例子。 12345 12345 12345 具有二进制表示 [ 11000000111001 ] [11000000111001] [11000000111001]。通过将二进制小数点左移 13 13 13 位,得到 12345 = 1.100000011100 1 2 × 2 13 12345=1.1000000111001_2\times 2^{13} 12345=1.10000001110012×213。为了用 IEEE 单精度形式来编码,我们丢弃开头的 1 1 1,并在末尾增加 10 10 10 个 0 0 0 来构造小数字段 [ 10000001110010000000000 ] [10000001110010000000000] [10000001110010000000000]。为了构造阶码字段,我们用 13 13 13 加上偏置量 127 127 127,得到 140 140 140,其二进制表示为 [ 10001100 ] [10001100] [10001100]。再加上符号位 0 0 0,我们就得到二进制的浮点数 [ 01000110010000001110010000000000 ] [01000110010000001110010000000000] [01000110010000001110010000000000]。下图展示了整数值 12345 12345 12345 和单精度浮点值 12345.0 12345.0 12345.0 在位级表示上的关系。
可以看到,整数部分和浮点表示的小数部分是高度匹配的。
2.4.4 舍入
因为表示方法限制了浮点数的范围和精度,所以浮点运算只能近似地表示实数运算。下图举例说明了四种舍入方式。向偶数舍入是默认的方式,对于常规值,采取四舍五入的决策;对于两个数的中间值,则采取将数字向上或者向下舍入,使得结果的最低有效数字是偶数。所以,这种方法将 1.5 1.5 1.5 和 2.5 2.5 2.5 都舍入到 2 2 2。
为什么默认采取向偶舍入呢?因为向偶舍入在大多情况下可以避免统计误差。
向偶舍入也可以不舍入到整数。例如, 1.2349999 1.2349999 1.2349999 舍入到 1.23 1.23 1.23, 1.2350001 1.2350001 1.2350001 舍入到 1.24 1.24 1.24,因为这两个数都不是正中间的数,采取四舍五入的方式。而 1.2350000 , 1.2450000 1.2350000,1.2450000 1.2350000,1.2450000 都舍入到 1.24 1.24 1.24。
2.4.5 浮点运算
浮点加法满足交换律,不满足结合律。例如,使用单精度浮点,表达式 (3.14+1e10)-1e10
求值得到
0.0
0.0
0.0,因为舍入导致值
3.14
3.14
3.14 丢失。而表达式 3.14+(1e10-1e10)
将得到
3.14
3.14
3.14。
2.4.6 C 语言中的浮点数
所有的 C 语言版本提供了两种不同的浮点数据类型:float
和 double
。在支持 IEEE 浮点格式的机器上,这些数据类型就对英语单精度浮点和双精度浮点。另外,这类机器使用向偶舍入的方式,
当在 int、float、double
格式之间进行强制类型转换时,程序改变数值和位模式的原则如下:
- 从
int
到float
,数字不会溢出,但是可能被舍入(丢失精度)。 - 从
int、float
到double
,不会溢出也不会丢失精度。 - 从
double
到float
,可能溢出,也可能丢失精度。 - 从
float、double
到int
,值将会向 0 0 0 舍入。例如, 1.999 1.999 1.999 将被转换成 1 1 1,而 − 1.999 -1.999 −1.999 将被转换成 − 1 -1 −1。进一步来说,值可能会溢出,C 标准未定义这种情况。与 Intel 兼容的微处理器指定位模式 [ 100 ⋯ 00 ] [100\cdots00] [100⋯00](字长为 w w w 时的 T M i n w TMin_w TMinw)为整数不确定值(不同机器可能不一样,如笔者在 vsc 上为 [ 011 ⋯ 11 ] [011\cdots11] [011⋯11])。当从浮点数到整数的转换,不能为浮点数找到一个合理的整数近似值时,就会产生这样一个值。例如,(int)1e10
将得到-2147483648
( 0 x 80000000 0x80000000 0x80000000)。