位(bit,符号为 b)是计算机中最小的信息单位。
字节(byte,符号为 B)是基本的存储和寻址单位。8 位一个字节。
参考计网
字是一个架构层面的约定,一个字可能表示 2、4 或 8 字节。
字长(也成为机器字长)指 CPU 内部整数运算的数据通路的宽度,通常也等于通用寄存器的宽度,也即指针占用的内存空间大小。反应计算机一次能处理的整数数据的位数。常见的是 32 位和 64 位。
小/大端法:
对齐
为了提高内存读写效率,数据之间需要进行对齐。对齐原则是任何 字节的基本对象的地址必须是 的倍数。
C 语言中的结构体内有多种数据类型。结构体的内存布局需要遵循下述对齐规则:
每个成员的起始地址必须是其对齐值的整数倍。
整个结构体的大小必须是其最大成员对齐值的整数倍(不足则在尾部填充)。
这么做的原因是,1 2 4 8 这四个数,后面的数字都是前面的数字的倍数,比如按 8 对齐之后自然满足按 2 对齐,但是按 2 对齐后未必能是按 8 对齐的,所以我们要取对齐的最大值
比如:
struct P1 {int i;char c;int j;char d;};| 偏移 | 0 | 4 | 8 | 12 | 16 | 对齐 |
|---|---|---|---|---|---|---|
| 内容 | int i; |
char c; + 填充 |
int j; |
char d; + 填充 |
4 |
注意最后大小不是 9,d 后面还有 3 字节的填充。这么做是因为我们希望做到 P1 类型后面再跟一个 P1 类型的话,后面 P1 类型的第一个成员也满足对齐
struct P2 {int i;char c;char d;long j;};| 偏移 | 0 | 4 | 5 | 8 | 16 | 对齐 |
|---|---|---|---|---|---|---|
| 内容 | int i; |
char c; |
char d; + 填充 |
long j; |
8 |
struct P3 {short w[3];char c[3];};| 偏移 | 0 | 6 | 10 | 对齐 |
|---|---|---|---|---|
| 内容 | short w[3]; |
char c[3]; + 填充 |
2 |
这个例子最后的填充说明,数组需要对齐的倍数为数组元素的大小而并非整个数组的大小
struct P4 {short w[5];char *c[3];};| 偏移 | 0 | 16 | 40 | 对齐 |
|---|---|---|---|---|
| 内容 | short w[5]; + 填充 |
char c[3]; |
8 |
struct P5 {struct P3 a[2];struct P2 t;};| 偏移 | 0 | 24 | 40 | 对齐 |
|---|---|---|---|---|
| 内容 | struct P3 a[2]; + 填充 |
struct P2 t; |
8 |
数字后添加后缀字母来标识进制:B 表示二进制、O 表示八进制、D 表示十进制(通常省略)、H 表示十六进制。
不同进制转换
二进制与八进制/十六进制互换
整数部分,从小数点向左,每 位(八进制)或每 位(十六进制)分为一组(如果向左不足 位或者 位则在高位补 )。小数部分,从小数点向右每 位或 位分成一组,不足则在低位补 。分组完成后,将每组直接替换为对应的八进制或十六进制数即可。
八进制与十六进制转为二进制时,即把对应的数码变为 位或是 位的二进制数。
八进制和十六进制相互转换时,最好是先转成二进制,然后再进行转换。
十进制转任意进制数
对于整数部分使用除基取余法,不断除以目标进制的基数,记录余数,直至商为 。最先得到的余数为最低位,最后得到的为最高位。
对于小数部分使用乘基取整法,不断乘以基数,记录整数部分,直至小数部分为 。最先得到的整数为最高位,最后得到的为最低位。
并非所有的十进制小数都可以被有限位二进制精确表示,只有可以被表示成形如 的分数的十进制小数才可以。像是 就不行。对应地,任何有限位二进制小数都对应一个分母为 的幂的分数,因此总能精确地转换为十进制小数。
机器数:数字在计算机中的表示。
真值:机器数所代表的实际数值。
二进制表示中,最左边的位为符号位,剩余的位为数值部分。数值部分的最左边的位为最高有效位,最右边的位为最低有效位。
最高有效位和最高位有没有区别感觉不太好把握。比如这里的符号位是最高位,但不是最高有效位。
机器的内部表示中并没有小数点,小数点是人为的(相当于数据类型)。
最高位表示数的符号,其余各位表示数的绝对值。
其中 为真值。字长为 。
表示范围为 ,关于原点对称。
零的原码表示有正零和负零两种。
原码实现乘法和除法比较简单,实现加法和减法比较困难。
加法规则:如果是同号的,结果的符号位就取这个相同的符号,然后再把数值位相加,如果相加完了之后向符号位进位了,就说明发生了溢出。如果是异号的,比较两者的绝对值,结果的符号位取绝对值最大的数的符号,然后再把绝对值较大的数值位减去绝对值较小的数值位。
减法规则:把被减数取相反数,再去应用加法规则。
由于原码的加法操作存在比较绝对值大小这一步,逻辑比较复杂,所以现代计算机普遍使用补码。
又称为 2 的补码,或是模 2 补码。
其中 为真值。字长为 。
等价地,无论是正数还是负数,均有 。将正数和负数均视为一个无符号整数进行运算,自然溢出,就相当于是在模 意义下进行运算,所以补码表示可以直接运算。
从位的角度上,最高位的贡献为 。
表示范围为 ,比原码多表示一个负数 。
正数的补码表示和原码相同。负数的补码表示为其对应的正数的补码表示按位取反,再加 。
变形补码
也被称为补码的双符号位表示,或者是模 4 补码。
变形补码在高位有两个符号位,两个符号位相同。相加时可以直接根据这两个符号位来判读溢出情况,也就是后面的双符号位检测溢出。
模 4 补码有模 2 补码的全部有点且更容易检查加减,包括乘除运算中的溢出问题。
由于正常的模 4 补码的两个符号位总是相同的,所以存储时也是只需要存储 1 个符号位,只有在 ALU 中运算时才需要两个符号位。
又称为 1 的补码。
反码可视为真值转成对应补码的中间表示。
正数的反码与其原码相同。负数的反码由其原码的数值部分按位取反(如果再加 就变成了补码)。
反码的 也是有两种表示方式。
移码是将真值 加上一个偏置值。如果字长为 位,偏置值通常取 :
表示范围为 。
在 IEEE 754 标准中,阶码也用移码表示,但是 位的阶码的偏置值为
移码的符号位取反即可得到补码。
表示范围为 。移码全 时,对应的真值的最小值 。移码全 时,对应真值的最大值 。
正数的原码、反码、补码相同,移码不同。
移码能直接在位表示上反应出大小关系,其他的编码方式则不能。原码能够直观的比较大小,数值部分即为绝对值。补码和反码,在符号位相同的前提下,数值部分越大,其对应真值也越大。
long (long int),在 32 位系统中为 32 位,在 64 位系统中通常为 64 位。
相同字长的转换,不改变位表示,仅改变解释方式。
有符号数转无符号数
无符号数转有符号数
符号位为 ,两者转换前后相同。反之,转化前后相差 。
有符号和无符号同时出现时
在C语言中,有符号和无符号同时出现时,会隐式地将有符号转为无符号。
短类型转换为长类型:位扩展。
零扩展:无符号整数扩展时,高位补0。
符号扩展:有符号整数扩展时,补原最高位,类似逻辑右移。
扩展前后对应的真值是不变的。
长类型转换为短类型,直接把高位截断。
当既有有符号和无符号的转换,又有扩展或者截断时,要先改变大小,再进行有符号和无符号之间的转换。这是C语言标准规定的。
现代系统普遍采用 IEEE754 浮点数标准。
对于一个小数 ,我们可以将其表示为一个二进制的小数:
然后,类似于科学计数法,我们把小数点挪到前面去,仅保留最左边的有效位:
必然是 ,因此我们不存储 ,只存储后面的 个位。这些位我们称之为尾数,记为 ,使用原码编码。
我们把小数点向左移动了 位。使用移码来编码这个 。我们将移码编码后的机器数称之为阶码,记为 。
阶码不用补码的原因:IEEE754 的设计者在最开始希望浮点数的大小能直接通过位的角度来比较,而补码显然不满足这一点。
IEEE 754 中 位的阶码的偏置值为 。
再使用单独的符号位来表示正负。
浮点数的表示为:
其中单精度格式对应的补码偏置值为 ,双精度格式对应的补码偏置值为 。
对于单精度浮点数,其对应的真值为:
对于双精度浮点数,其对应的真值为:
上面两个式子中的 表示基数。浮点数中的基数固定为 。
阶码部分如果全为 或者是全为 就不表示规格化数了。
非规格化数用于表示非常接近于 的小数。
规格化数中,尾数实际上是有一个隐藏位 ,而非规格化数没有隐藏位。同时,非规格化数的表示中,阶码看起来 ,但这里 取
于是单精度非规格化数的真值为:
双精度非规格化数的真值为:
| 阶码 | 尾数 | 说明 |
|---|---|---|
| 全 | 全 | 表示真值 。IEEE 754 中的 有 和 两种。 |
| 全 | 不全 | 表示非规格化数 |
| 既不全 也不全 | —— | 表示规格化数 |
| 全 | 全 | 表示 。根据符号位,分成 和 两种 |
| 全 | 不全 | 表示 NaN,一般是计算发生了错误,比如对负数开根。 |
一些架构可能会在 NaN 的尾数部分编码更多的信息来描述 NaN 是如何产生的。但这属于标准之外的东西,不同的架构有不同的实现。
位数相同的定点数和浮点数,定点数能表示的数值更多。因为 位编码最多表示 个不同的比特模式。定点数通常能表示 个互异的有效数值,而浮点数中部分编码用于表示 、,NaN 等特殊数值,实际可以表示的有效实数个数小于 。
对阶
首先要对阶,让两个操作数的小数点位置是对齐的,便于后面直接对尾数相加减。
对阶的原则是小阶向大阶看齐。阶码较小的操作数可以右移尾数部分(即左移小数点)来使得阶码增加。
如果阶码较小的操作数是规格化数的话,尾数在右移的时候,隐藏位也会被移进来。
尾数中由于右移被移出去的部分不会被直接丢掉,而是会放到辅助位上,用于后面的舍入。
不能是大阶向小阶看齐,这样的话相当于小数点右移,小数点左边的数没地方放。
尾数加减
对于规格化数,运算的时候要把隐藏位还原到尾数部分,也就是要带着隐藏位来进行计算。
用于舍入的辅助位也要参与运算。
本质上就是定点原码小数的加减运算。不过实际上计算机的硬件上没有原码运算的硬件实现。硬件会直接把尾数视为无符号数,送入通用 ALU 进行加减运算。
尾数规格化
先对尾数的运算结果进行规格化:
右规
尾数相加之后可能得到这种结果:1x.xx...x,小数点左边有两个数,此时需要进行右规。
尾数右移一位,阶码加 。然后最高位 去掉,作为隐藏位。
最后一位被移出时,需要考虑舍入。
尾数相加之后,小数点左边最多只有两个数,所以必然只需要右移一次。
左规
尾数相减之后可能得到这种结果:0.00...01x...x,此时需要进行左规。
尾数每左移一次,阶码减 ,可能需要左移多次,直到第一位 移到小数点左边。
左移的时候要把辅助位的也给移进来。
规格化之后,进行舍入(不是规格化之前进行舍入)。
舍入之后,有可能因为给尾数最低位上面加 了,导致尾数再次不是规格化的了,于是还需要再右规一次。
在对阶和右规的过程中,尾数右移可能导致低位丢失。为了保证精度,低位丢失时需要对浮点数进行舍入。
IEEE754 引入如下三个辅助位来指导舍入:
IEEE754 定义了如下四种可选择的舍入模式:
值得注意的是,这里的 是必要的,可以使得左规之后再舍入是对的。需要左规时,一定是两个浮点数相减。设大的浮点数为 ,小的浮点数为 ,那么会有如下两种情况:
当两个数的阶码相差很大( 时)
的尾数必须右移至少 位,此时 的低位会掉进 G、R、S 中。但是,比如 尾数形如 1.000..., 右移两位后最大也只是 0.011...,相减,得到 0.1xxx... 或者是 1.0xxx 这个样子。总之,后面再左规的时最多只需要左移 位。
左移的时候,G 被移进来了。原来的 R 到了现在的 G 的位置,由于原来的 R 就是精准的,所以现在的 G 还是精准的。
原来的 S 到了现在的 R 的位置,原来的 S 不精准,所以现在的 R 也不精准。但是,上面的舍入规则看的是“舍入位为 或者粘滞位为 ”或者是“如果保护位和粘滞位均为 ”,而如今的 R 即使不精准,但是仅凭 R 本身(也就是原来的 S)也能精准地判断出这两个规则谁是符合的,所以不影响舍入。
而假如说我们没有 R,那么会导致左移之后 G 就是不准的了,G 到底是 0 还是 1 无法判断,也就无法判断怎么舍入。
如果两个数的阶码相差为 或是 时,此时两个数非常接近,相减之后出现了大量的 ,比如 0.0000001。此时需要左移很多位来左规。
但是:
对于两个单精度浮点数相加,如果两个操作数阶码之差的绝对值 ,则结果直接取阶码较大的操作数。
证明:阶码相差大于等于 的话,即使较小操作数是规格化数,其最高位的 (也就是隐藏位)也被右移到了较大操作数 R 位置及其之后。此时相加,无论较小操作数是什么,都改变不了结果的 G 位,结果的 G 位永远为 ,永远都是直接向下舍入的。
而如果是两个单精度浮点数相减,那么阶码之差绝对值要 才能使得结果直接取阶码较大的操作数。
证明:
首先,如果阶码之差为 ,那么被减数的 GSR 部分为
000,减数对阶之后 GSR 部分为0xx。可能出现这种情况:被减数的最低有效位为 ,减数的 GSR 部分为010。那么相减之后,结果的最低有效位变为了 (被 GSR 部分借位了),然后结果的 GSR 部分为100。此时向偶舍入,结果的最低有效位还是为 。结果和被减数不一致。如果阶码之差为 ,那么有两种情况:
- 减数的 GSR 部分为
000,这个是显然的。- 减数的 GSR 部分为
001,相减之后,会从 GSR 前面的位借 ,结果的 GSR 变为110,于是又向上舍入,导致 GSR 前面的位又进了 ,于是结果和被减数保持一致。
这个题说的是加减,但是答案给的 ,我认为不太对。
浮点数溢出本质上是阶码超出可表示范围而导致的(尾数超过可表示范围属于舍入的问题)
上溢(也直接称为溢出)
当浮点运算结果的绝对值超过最大规格化数的时候发生。比如浮点数相加后结果 ,或者是舍入后结果又 ,这两种情况下都会导致左规,从而使得阶码 。如果原阶码已为最大正规格化值,那么溢出就发生了。
如果结果为正且大于最大规格化正数,则称为正溢出。如果结果为负且小于最小规格化负数,则称为负溢出。
IEEE754 对溢出的处理规则:
下溢
当浮点运算结果的绝对值小于最小规格化正数但不为 时发生。一般是发生在尾数相减,然后左规时,尾数不断左移,然后阶码一直减 。当减到 (对于单精度)或是 (对于双精度)时,再减下去,就变成了非规格化数,此时我们称之为发生了渐进下溢。
非规格化数的设计可以使得规格化数平滑地进入非规格化数。同时非规格化数的分布是均匀的,
如果运算结果小到连非规格化数都表示不下,此时就说明发生了指数下溢,IEEE754 对其进行如下处理:
浮点数和整数混合运算时,会先把整数隐式转换为浮点数。
当 int 转 float 时,虽然不会发生溢出,但是由于 float 尾数(含隐藏位)仅 24 位有效,而 int 为 32 位,因此当整数的二进制有效位超过 24 位时,需要进行舍入处理,导致精度损失。
int 和 float 转 double,由于后者有效位更多,所以通常能精确地表示。
double 转 float 时,一方面 float 的表示范围较小,大数值转换可能会发生溢出(涉及到阶码)。另一方面,float 的尾数有效位变少,还会发生舍入误差(涉及到尾数)。
float 或 double 转 int 时,小数部分会直接丢弃。同时,如果浮点数值超出 int 表示范围,还会发生整数溢出(未定义行为)。
无符号整数
位数为 时, 和 相加,得到 。即如果要溢出了就减去 。
设得到的结果为 ,如果 或者 ,则说明发生了溢出。
有符号整数
位数为 时, 和 相加,如果发生了正溢出就减去 ,如果发生了负溢出就加上 。
设得到的结果为
输入:当前位的两个加数 和 ,来自低位的进位 。
输出:本位的加和结果 ,向高位的进位 。
又称行波进位加法器。
最后一位溢出,直接舍掉,相当于进行模 ( 为加法器位数)的加法。
又称为先行进位加法器/超前进位加法器。
考虑到每一位的进位值:
我们把不依赖前面的进位值的部分提出来:
然后每一位的进位值展开:
这样每一个进位值就都是独立的了,可以在电路上并行计算。
这样展开之后的算式看起来很复杂,实际上电路也确实复杂(),但是电路是分级的,同一级上可以并行计算,所以速度上并不会太慢:
电路大概会分成 级,于是进行一个加法操作只需要 的时间,优于串行进位加法器的 的时间。
在完成加法操作的情况下还生成如下的标志:
溢出标志:OF
,其中 为向符号位的进位输入, 为符号位的进位输出。
用于判断有符号数(将输入的二进制位视为补码时)的加法运算是否溢出,OF 为 1 表示溢出,为 0 表示没溢出。对于无符号数的运算来说没有意义。
只有两个同号的数相加才会发生溢出(相减视为对相反数相加)。如果是两个正数相加发生溢出,那么 ,,得到 。如果是两个负数相加发生溢出,在 这一位上不进位,,。
溢出标志还有别的计算方法:
当两个操作数的符号位相同,但是结果的符号位不同时,说明发生溢出(正数相加溢出必然得到负数,负数相加溢出必然得到正数)
搞两个符号位,, 是高位, 是低位。 即为原来的符号位, 和 相同,相当于是一个备份。对于 也同理。运算后,观察运算结果的符号位 。
道理类似 。 表示正数相加没溢出, 表示负数相加没溢出, 表示正数相加溢出, 表示负数相加溢出。
符号标志:SF
,即结果的符号位。
用于判断运算结果的正负性,SF 为 1 表示负数,为 0 表示正数。
零标志:ZF
用于判断结果是否为 0。
进位/借位标志:CF
,其中 为初始的进位输入, 为符号位的进位输出。
用于判断无符号数(将输入的二进制位单纯地视为无符号数时)的加法运算是否溢出,CF 为 1 表示溢出,为 0 表示没溢出。对于有符号数的运算来说没有意义。
在做加法时,有 ,如果溢出了,那么必然有 。在做减法时,有 ,减法在没溢出时,反而是 , 则反而是说明减法溢出了。
这些标志可以用来判断两个数字的大小。比如现在对于两个数字 和 进行 的运算,得到了标识位:
如果 ,说明
如果
硬件中的加法是在模 意义下进行的。对于一个二进制的 位数 ,对其按位取反,再加 ,即可得到 在模 意义下的减法逆元。
在做减法的时候,只需要将减数按位取反,同时将 设为 ,即可用加法器做减法。
上面这些,无论数字是有符号还是无符号都适用。
数字的有无符号性取决于上层对二进制编码的解释。由于补码有 ,所以其天然地适配于硬件上自然的加法。
ALU
提供按位或和按位与操作,然后和加法操作合在一块,根据多路复用器来选择输出哪个操作的结果。
实际上的 ALU 还会包含 xor 和 not 操作。不过只有 and 和 or 也能完成 xor 和 not(对 -1 取 xor 即可)。
ALU 中的核心组件是加法器。
对于无符号数使用逻辑移位:
对于有符号数使用算数移位:
移位运算就不在 ALU 里面做了,而是有专门的硬件。
朴素的做法是使用移位寄存器,可以在每个周期移位一次。下图是一个四位的移位寄存器,每个时钟周期右移(向 方向)一次。
当然这样的效率非常低。更好的选择是桶形移位器。下图是一个八位的桶形移位器:
din 为数据输入。shamt 表示要移位的次数。L/R 表示左移还是右移,为 为左移,为 为右移。A/L 表示逻辑移位还是算数移位,为 为算数移位,为 为逻辑移位。
对应电路:
和超前进位加法器类似,也是通过电路的分级来减少时间开销。第 层来把输入进行 的移位,相当于对 shamt 进行二进制拆分来做。同时,桶形移位器是纯组合电路实现的,一个周期内就能完成。
如果手算一个二进制数(不考虑符号),可以直接沿用十进制乘法的方法列竖式计算。
根据这个方法可以得到无符号数乘法的运算电路:
图中的 、 和 实际上是同一个寄存器的最高位、高位部分和低位部分。
对于 这个运算,初始时:
接下来会不断循环如下的过程:
这个过程相当于是最后的结果右移,由于被乘数不动,所以被乘数相对于结果是左移的,于是就和上面竖式的过程对上了。
如果最后 中不全为 的话,则说明最后的运算结果用 位存不下,发生溢出。
这个电路基于 Booth 算法。Booth 算法其实是一个优化算法,用来减少乘法过程中相加的次数,只不过可以恰好用于处理补码中的符号位。
比如说我们有二进制 ,这个可以表示为 ,在对第 位(右边第一个是第 位)相减时,会一直向前借位,然后就导致了中间这一连续的一段变为了全 。
考虑一组从位位置 到位位置 的连续的 (),这一部分对乘积的影响有如下两种形式:
Booth 算法就是通过把第一种形式转为第二种形式,减少了乘数中的 的个数,从而使得加法次数变少,不过又同时引入了减法操作。
和上面的电路差不多,区别在于,这里在 的最低位的右边又引入了一个辅助位 。 初始值为 。、 和 还是应该在右移的时候视为一个整体。
在循环时, 的最低位 和辅助位 组成一个两位的二进制码,送入控制逻辑:
同时,这个电路的右移是算数右移。
Booth 算法能够处理掉补码中符号位的原因是,对于负数,其补码表示会有一段前缀 段。而上述的运算过程中,遇到这个前缀 段时会先减去当前位的贡献。正常来说,后面我们扫过去连续 段,再加上更高位的贡献后,总贡献就相当于是原来连续 段内所有 应该有的贡献。但是由于我们这个是前缀 段,后续是碰不到 的情况了,所以只有减的贡献,没有加的贡献,而这个减的贡献又恰好对应于补码。
如果最后, 中所有的位和 中的最高位符号不同,则判定为溢出。
发生溢出后,CPU 会将 和 同时置 。
此外 CPU 不进行任何处理。乘法的溢出处理属于软件层面的操作,如果需要对溢出进行特殊处理的话,可以在乘法指令后面加入判断逻辑。
在编程语言上,对于 和 相乘,令其结果为 。如果 为 ,显然不会发生溢出。反之,如果 p / x != y 则说明发生溢出。
无符号
对于除以 且下取整,直接逻辑右移 位即可。
有符号
对于除以 ,如果直接算数右移 位,那么得到的结果是向下取整的。
但C语言中的负数做除法时,并不是向下取整,是向零取整的。
当被除数为正时,没什么问题。
当被除数为负时,我们需要先加上一个偏置值 ,再算数右移 位。
二进制除法手算也是和十进制是一样的,不断地试商:
对应的电路:
对于 这个操作,初始时需要先进行一系列特判:
如果是无符号数,我们有如下的过程:
首先对寄存器进行初始化:
然后不断进行下面的循环:
如果是有符号数,运算过程会存在如下差异:
当发生除 或者是商溢出的异常时,CPU 会设置异常标识,然后由硬件自动地通过中断向量表跳转至预设的异常处理程序。
0 也行,所以选择 D。
A 选项是商溢出的情况,B 是被除数绝对值小于除数绝对值的情况,这两种情况都是会被开始时特判调的,所以理论上执行时间都挺短的。不过商溢出的情况应该更容易特判,所以 A 应该更对,但是答案给的 B ,似乎有点问题。
采用规格化的浮点是最主要是为了增加数据的表示精度。
对于 Ⅰ,x + y 是可能发生整数溢出的,而 dx + dy 是不会发生溢出的。
对于 Ⅳ,可以直接用舍入那里的结论,f 和 d 的阶码之差显然大于了 ,因此 d+f==d。
注意 employee[1].id 位于 C4,同时意味着 78H 位于 C4
(感觉答案有点问题,在第二步那里没有带上隐藏位进行运算)
对于 (2),考虑到 x 和 y 都是可以用机器数表示出来的,所以可以先计算 x+y 和 x-y 然后再转成浮点数机器码。