北航计算机学院-计算机组成原理课程设计-2020秋
PreProject-数制
本系列所有博客,知识讲解、习题以及答案均由北航计算机学院计算机组成原理课程组创作,解析部分由笔者创作,如有侵权联系删除。
介绍
此处指的数制即计算机是存储数字的方式。鉴于课程需要,这里暂且只介绍整数的表示,感兴趣的同学可以去本节最后的参考资料处阅读相关材料。对于整数表示常用的原码、反码和补码,同学们需要清楚三种表示之间、三种表示和十进制有符号数的转换方式,同时熟知三种表示方法的特点与问题,特别是补码,在CPU开发环节,它将始终与同学们为伴!
进制
下面来简单介绍一下数学上进制的概念。
进位制(positional notation 或 place-value notation),是一种编码数的方式。在这里不准备过于形式化的介绍这个概念,只是希望同学有一个在实用层面比较准确的概念即可。在下面的说明中,默认十进制表示是个先验概念,如无特殊记号指明,所有数都是以十进制表示的。由于习惯问题,在下面的说明中,位都是从 0 0 0 开始编号的,不难修改相关定义使得位从 1 1 1 开始编号。
一个
n
n
n 位
b
b
b 进制数(base-b number)为一个字符串
(
b
n
−
1
b
n
−
2
…
b
1
b
0
)
b
(b_{n−1}b_{n−2}…b_1b_0)_b
(bn−1bn−2…b1b0)b,其中
b
i
∈
{
0
,
1
,
…
,
b
−
1
}
b_i∈\{0,1,…,b−1\}
bi∈{0,1,…,b−1}。定义其值为
(
b
n
−
1
b
n
−
2
…
b
1
b
0
)
b
=
∑
i
=
0
n
−
1
b
i
×
b
i
。
(b_{n−1}b_{n−2}…b_1b_0)_b=\sum\limits_{i=0}^{n−1}b_i×b^i。
(bn−1bn−2…b1b0)b=i=0∑n−1bi×bi。
其中
b
i
b^i
bi 叫做第
i
i
i 位的位权。当不特殊指明位数时,代表在当前上下文中,位数不重要。一个 b 进制数也叫作一个基数(base)为
b
b
b 的数。
比如 ( 10 B ) 16 = 1 × 1 6 2 + 0 × 1 6 1 + 11 × 1 6 0 = 267 (10B)_{16}=1×16^2+0×16^1+11×16^0=267 (10B)16=1×162+0×161+11×160=267,给出了 267 267 267 在十六进制下的表示。在十以上进制中,为了不造成解读上的困难,通常会使用字母来表示 10 10 10 及其以上的数。比如十六进制中的 A , B , C , D , E , F A,B,C,D,E,F A,B,C,D,E,F 分别代表了 10 , 11 , 12 , 13 , 14 , 15 10,11,12,13,14,15 10,11,12,13,14,15。字母大小写一般不重要。
由于十六进制的广泛使用,人们规定了一种十六进制数的表示,即在数前加 0 x 0x 0x 以区分十进制数。比如 ( 5 a ) 16 (5a)_{16} (5a)16 可以写作 0 x 5 a 0x5a 0x5a。
进制转换
既然一个数可能有不同的表示,那么紧接着出现的问题就是,这些不同表示如何相互转换,也就是如何进行进制转换。
由于本课程的需求,在这里主要介绍十进制与二、八、十六进制的相互转化,以及二、八、十六进制之间的相互转化。
十进制与二、八、十六进制的相互转化
对于二、八、十六进制向十进制的转化,直接采用定义式
(
b
n
−
1
b
n
−
2
…
b
1
b
0
)
b
=
∑
i
=
0
n
−
1
b
i
×
b
i
。
(b_{n−1}b_{n−2}…b_1b_0)_b=\sum\limits_{i=0}^{n−1}b_i×b^i。
(bn−1bn−2…b1b0)b=i=0∑n−1bi×bi。
即可。
举例来说 0 x 1 b 3 c = 1 × 1 6 3 + 11 × 1 6 2 + 3 × 1 6 1 + 12 × 1 6 0 = 6972 0x1b3c=1×16^3+11×16^2+3×16^1+12×16^0=6972 0x1b3c=1×163+11×162+3×161+12×160=6972,再比如 ( 10110 ) 2 = 1 × 2 4 + 0 × 2 3 + 1 × 2 2 + 1 × 2 1 + 1 × 2 0 = 22 (10110)_2=1×2^4+0×2^3+1×2^2+1×2^1+1×2^0=22 (10110)2=1×24+0×23+1×22+1×21+1×20=22。
对于十进制向二、八、十六进制的转化,则可以运用以下事实递归计算。设 x x x 为一非负整数,其 b b b 进制表示为 ( b n − 1 b n − 2 … b 1 b 0 ) b (b_{n−1}b_{n−2}…b_1b_0)_b (bn−1bn−2…b1b0)b,则有等式 x m o d b = b 0 x\space mod\space b=b_0 x mod b=b0 以及 ⌊ x / b ⌋ = ( b n − 1 b n − 2 … b 1 ) b ⌊x/b⌋=(b_{n−1}b_{n−2}…b_1)_b ⌊x/b⌋=(bn−1bn−2…b1)b 成立。
举例来说,将 6972 转化成十六进制,步骤如下:
- b 0 = 6972 m o d 16 = 12 , ⌊ 6972 / 16 ⌋ = 435 b_0=6972\space mod\space 16=12,⌊6972/16⌋=435 b0=6972 mod 16=12,⌊6972/16⌋=435;
- b 1 = 435 m o d 16 = 3 , ⌊ 435 / 16 ⌋ = 27 b_1=435\space mod\space 16=3,⌊435/16⌋=27 b1=435 mod 16=3,⌊435/16⌋=27;
- b 2 = 27 m o d 16 = 11 , ⌊ 27 / 16 ⌋ = 1 b_2=27\space mod\space 16=11,⌊27/16⌋=1 b2=27 mod 16=11,⌊27/16⌋=1;
- b 3 = 1 m o d 16 = 1 , ⌊ 1 / 16 ⌋ = 0 b_3=1\space mod\space 16=1,⌊1/16⌋=0 b3=1 mod 16=1,⌊1/16⌋=0。
所以 6972 = 0 x 1 b 3 c 6972=0x1b3c 6972=0x1b3c。十进制向二、八进制的转换可以类似得到。
二、八、十六进制间的相互转化
由于二、八、十六进制的基数都是 2 的幂,因此转化起来较为简单。
由于 16 = 2 4 16=2^4 16=24,因此将二进制转化为十六进制,只要从低位开始,以四位为一组,分别转化到对应的十六进制即可。举例来说, ( 11010001101011 ) 2 = ( 11 0100 0110 1011 ) 2 = 0 x 346 b (11010001101011)_2=(11 \space 0100 \space 0110 \space 1011)2=0x346b (11010001101011)2=(11 0100 0110 1011)2=0x346b。
十六进制转二进制则是上述过程的逆过程,即将十六进制中的一位展开成四位二进制最后再拼起来即可。
二、八进制的互相转化同理。
至于八、十六进制的互相转化,可以将八进制转化成二进制,再将二进制转化成十六进制即可。
进制转换练习题
491 491 491 的 16 16 16 位二进制表示是什么?
请以类似 0000000000000000 0000000000000000 0000000000000000 的形式作答,不要有多余的空白字符。
答案: 0000000111101011 0000000111101011 0000000111101011
8613 8613 8613 的 4 4 4 位十六进制表示是什么?
请以类似 0 x 0000 0x0000 0x0000 的形式作答,使用小写字母,不要有多余的空白字符。
答案: 0 x 21 a 5 0x21a5 0x21a5
0 x 8 f 4 c 0x8f4c 0x8f4c 的十进制表示是什么?
请回答一个十进制数,不要有多余的空白字符以及前导 0。
答案: 36684 36684 36684
0 x 78 c b c 3 0x78cbc3 0x78cbc3 的 24 24 24 位二进制表示是什么?
请以类似 000000000000000000000000 000000000000000000000000 000000000000000000000000 的形式作答,不要有多余的空白字符。
答案: 011110001100101111000011 011110001100101111000011 011110001100101111000011
( 1011010110101001011110 ) 2 (1011010110101001011110)_2 (1011010110101001011110)2 的十六进制表示是什么?
请以类似 0 x 000000 0x000000 0x000000 的形式作答,使用小写字母,不要有多余的空白字符以及前导 0。
答案: 0 x 2 d 6 a 5 e 0x2d6a5e 0x2d6a5e
根据进制转换规则进行相应的计算即可。
原码
由于计算机采用开关元件的特点,现代计算机均使用二进制位的序列(bits)来表示数,包括小数。实际上,历史上俄罗斯曾经尝试开发过三进制计算机,想深入了解的可以自行搜索。
在数学上表示整数时,我们使用前导的负号(-)来表示负数;那么用二进制表示整数的最直观想法,就是使用一位来表示正负号,这种表示方法被称作原码。举例来说,设有一个 3 3 3 位的原码,则 [ 1 ] 原 = ( 001 ) 2 [1]_原=(001)_2 [1]原=(001)2, [ − 1 ] 原 = ( 101 ) 2 [−1]_原=(101)_2 [−1]原=(101)2。对于 n n n 位的原码,其可以表示的数的范围为 [ − 2 n − 1 + 1 , 2 n − 1 − 1 ] [−2^{n−1}+1,2^{n−1}−1] [−2n−1+1,2n−1−1]。
形式化来说,设使用
n
≥
1
n≥1
n≥1 位二进制位表示一个数
X
X
X,则其原码
[
X
]
原
[X]_原
[X]原 为
[
X
]
原
=
{
X
,
0
≤
X
≤
2
n
−
1
−
1
2
n
−
1
−
X
,
−
2
n
−
1
+
1
≤
X
≤
0
[X]_原=\left\{ \begin{array}{rcl} X, & & {0≤X≤2^{n−1}−1}\\ 2^{n−1}−X, & & {−2^{n−1}+1≤X≤0}\\ \end{array} \right.
[X]原={X,2n−1−X,0≤X≤2n−1−1−2n−1+1≤X≤0
注意到,上述表示中 0 0 0 有两种表示方法,这是原码带来的不便之一。其次,如果要做运算的话,原码是不能直接相加的,需要根据符号位做相应判断,同号绝对值相加,异号绝对值大的减去绝对值小的,最后置符号位。因此,在现代计算机中涉及整数的计算,一般不使用原码。
根据 Wikipedia 上的资料,由于原码十分直观,因此在早期的计算机中得以使用。除此以外,原码在数制中的另一大用处即为表示浮点数的有效数字,也是当今几乎所有计算机表示浮点数的标准。其具体考量留待补码时做进一步说明。
反码
设一二进制数可使用的位共 n ≥ 1 n≥1 n≥1 ,如果将负数的绝对值的二进制按位取反的话,我们就得到一个数的反码(one’s complement)。
对于 n n n 位的反码,其可以表示的数的范围为 [ − 2 n − 1 + 1 , 2 n − 1 − 1 ] [−2^{n−1}+1,2^{n−1}−1] [−2n−1+1,2n−1−1]。
举例来说,设有一个 3 3 3 位的反码,则 [ 1 ] 反 = ( 001 ) 2 [1]_反=(001)_2 [1]反=(001)2, [ − 1 ] 反 = ( 110 ) 2 [−1]_反=(110)_2 [−1]反=(110)2。
形式化来说,一个数
X
X
X 的反码
[
X
]
反
[X]_反
[X]反 为
[
X
]
反
=
{
X
,
0
≤
X
≤
2
n
−
1
−
1
2
n
−
1
+
X
,
−
2
n
−
1
+
1
≤
X
≤
0
[X]_反=\left\{ \begin{array}{rcl} X, & & {0≤X≤2^{n−1}−1}\\ 2^{n−1}+X, & & {−2^{n−1}+1≤X≤0}\\ \end{array} \right.
[X]反={X,2n−1+X,0≤X≤2n−1−1−2n−1+1≤X≤0
注意到, 0 0 0 在反码的表示中也有两种表示方法,分别为全 0 0 0 和全 1 1 1。
尝试后容易发现,现在可以通过加法直接运算两个数的反码,但是需要一次循环进位(end-around carry),即如果有进位溢出(指两个 n n n 位数相加产生了第 n n n 位的进位,位的标号从 0 0 0 开始),则需要将该溢出进位当作 1 1 1 加入结果中。具体例子请参见这里。
因为上述的不方便,所以现在的绝大部分计算机都不采用这种方式,而采用下一节所讲到的补码进行运算。
作为一个题外话,来谈谈反码运算溢出的问题。如果两个同号数相加变成一个异号数,则发生溢出,两个异号数相加不可能发生溢出。
补码
为了弥补反码的缺点,人们发明了补码。一个 n n n 位的补码实际上是一个数在模 2 n 2^n 2n 意义上的表示。
对于 n n n 位的补码,其可以表示的数的范围是 [ − 2 n − 1 , 2 n − 1 − 1 ] [−2^{n−1},2^{n−1}−1] [−2n−1,2n−1−1]。
形式化来说,一个数
X
X
X 的补码
[
X
]
补
[X]_补
[X]补 为
[
X
]
补
=
{
X
,
0
≤
X
≤
2
n
−
1
−
1
2
n
+
X
,
−
2
n
−
1
≤
X
≤
0
[X]_补=\left\{ \begin{array}{rcl} X, & & {0≤X≤2^{n−1}−1}\\ 2^{n}+X, & & {−2^{n−1}≤X≤0}\\ \end{array} \right.
[X]补={X,2n+X,0≤X≤2n−1−1−2n−1≤X≤0
注意到,在补码中
0
0
0 只有唯一的表示,即全
0
0
0。因为
2
n
2^n
2n 是一个
n
+
1
n+1
n+1 位的数,而补码长度只有
n
n
n,舍弃最高位之后恰好又是
0
0
0,所以
0
0
0 的表示是唯一的。由于补码的运算本质上是在模
2
n
2^n
2n 意义下的运算,而不处理进位溢出,恰好相当于模
2
n
2^n
2n,所以对于补码来说只需做正常的加法即可。
举例来说,设有一个 3 3 3 位的补码,则 [ 1 ] 补 = ( 001 ) 2 [1]_补=(001)_2 [1]补=(001)2, [ − 1 ] 补 = ( 111 ) 2 [−1]_补=(111)_2 [−1]补=(111)2,而 [ 0 ] 补 = [ 1 ] 补 + [ − 1 ] 补 = ( ( 001 ) 2 + ( 111 ) 2 ) m o d 2 3 = ( 000 ) 2 [0]_补=[1]_补+[−1]_补=((001)_2+(111)_2)mod2^3=(000)_2 [0]补=[1]补+[−1]补=((001)2+(111)2)mod23=(000)2。
对于一个负数,知道其绝对值,如何得到其补码呢?只需对其绝对值的补码按位取反然后加一即可。按位取反即用 2 n − 1 2^{n−1} 2n−1 减去其绝对值, + 1 +1 +1 正好补上多减的 1 1 1。
因为 − X = 2 n − ( 2 n + X ) −X=2^n−(2^n+X) −X=2n−(2n+X),所以如果想得到一个负数的绝对值,只需要用 2 n 2n 2n 减去其补码表示即可,亦即按位取反再加一。
补码的溢出判定准则和反码相同:两个同号数相加如果得到一个不同号数,则发生溢出,两个异号数相加不会出现溢出。在实际判断中,为了不区分是否是同号数相加还是异号数相加,设第 n n n 位符号位(此处从 1 1 1 开始编号)产生的进位为 c a r r y M S B carry_{MSB} carryMSB,第 n − 1 n−1 n−1 位最高数位产生的进位为 c a r r y M S B − 1 carry_{MSB−1} carryMSB−1,发生溢出当且仅当 c a r r y M S B ⊕ c a r r y M S B − 1 = 1 carry_{MSB}⊕carry_{MSB−1}=1 carryMSB⊕carryMSB−1=1。该溢出准则引用自维基百科补码页中关于溢出检查的部分。
上段公式中的 MSB 为最高有效位 Most Significant Bit 的缩写,意为一个二进制数中有最大位权的位。举例来说,如果位是从 0 0 0 开始标号的话,则 n n n 位二进制数的 MSB 为第 n − 1 n−1 n−1 位。 ( 001 ) 2 (001)_2 (001)2 为一 3 3 3 位二进制数,其 MSB 为第 2 2 2 位。更详细的信息可以参见这里。
浮点数中的原码
在原码一节曾经提到过,在浮点数中表示有效数字时采用原码。既然整数的加减运算在补码中是一样的,那么为什么不同样使用补码表示浮点数中的有效数字呢?这里以个人之见谈谈其中的直觉。
根据这里的说明,在单位一不同时,用补码反而会较为麻烦。举例来说,乘二的幂次在原码表示中是十分简单的,只需将绝对值部分进行移位即可。而在补码表示中,乘二的幂可以通过左移解决,但是除二的幂就会出现问题,比如五位补码 [ − 9 ] 补 = ( 10111 ) 2 [−9]_补=(10111)_2 [−9]补=(10111)2 右移两位之后期望得到 [ − 2 ] 补 [−2]_补 [−2]补,但实际上得到的却是 [ − 3 ] 补 [−3]_补 [−3]补。为了得到正确结果,则需要将补码转换成原码再进行操作,所以在浮点数这种因为对齐幂次需要而对有效数字进行频繁调整的表示方法中,采取补码表示反而不如直接采用原码。这就是在浮点数标准中,表示有效数字使用原码而非补码的原因。
作为题外话,浮点数的幂因为考虑到比较的需要,采用的是移码(offset binary)表示,具体参见这里。
参考资料
- Digital Design and Computer Architecture 2nd, Chapter 1 From Zero to One, Section 1.4 Number Systems
- Wikipedia: Signed number representations
- Wikipedia: Two’s complement