本篇博客是《深入理解计算机系统》实验记录的第一篇,主要记录实验过程和心得。
实验名为DataLab,对应于书本的第二章:信息的处理与表示。
关于实验的方法请自行阅读实验文件压缩包中的README文件和代码文件中的前缀注释。
Q1
//1
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
int Xor = ~(~x & ~y) & ~(x & y);
return Xor;
}
Q1分析:
题目中要求只是用取反~和与&运算符来实现异或运算^。首先可以列出异或运算的真值表:
x | y | x^y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
根据离散数学里的知识,可以通过真值表将x^y使用与(∧)、或(∨)、非(对于二进制数而言也就是取反~)
x^y = (x ∨ y) ∧ (~x ∨ ~y) // 第一行和第四行的和之积表达式
= ~(~x ∧ ~y) ∧ ~(x ∧ y) // 德摩根律 (1.1)
将上述表达式1.1翻译为C代码即可。
Q2
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
return 1 << 31;
}
Q2分析:
这道题要求使用! ~ & ^ | + << >>运算符来实现最小补码值,最小的补码值(32位机器上)是:
1000 0000 0000 0000 0000 0000 0000 0000
也就是简单的将1向左移位31位到符号位即可。
Q3(*)
//2
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
int isTmax(int x) {
// return !((1 << 31) + x + 1); 错误的示例
int i = x + 1; /*Tmin或者0*/
x = x + i; /*如果x是Tmax,那么x此时为-1*/
x = x + 1; /*如果x是Tmax,那么x此时为0*/
i = !i; /*排除x == -1的情况*/
x = x + i; /*如果x == -1, i此时为1, 会对结果造成影响*/
return !x;
}
Q3分析:
上面注释了一个错误的示例,是我第一次写这道题时候的思路,后来在用dlc检查语法时报了错误(不允许使用<<,真是粗心啊)。然后就是重新开始解这道题,我注意到如果这样做了就会发现很难绕开-1这样一个特殊值,于是经常会得到这样的错误:
所以这道题最终的思路是要剔除-1这样一个例外值的干扰。完整思路如下:
//如果想证实一个数一定是Tmax,那么可以借助Tmax + 1 = Tmin来确定,它们是互为充要的。
Tmax + 1 = Tmin
=> Tmax - Tmin + 1 = 0
=> Tmax + (-Tmin) + 1 = 0
=> Tmax + Tmin + 1 = 0 //(3.1)
很多做法都是使用Tmax + Tmin + 1 = 0来判断的,但这里存在一个问题:Tmin如何表示?
Tmin = Tmax + 1, 但是并不能保证x = Tmax。
如果一厢情愿的将x当作Tmax来解,就会产生一个增根-1。
见3.1式,假设我们一厢情愿的去做,3.1式就变成了
x + (x + 1) + 1 = 0
x = -1时该式同样成立,此为增根来源,在解答时应该排除之。
所以这道题中i = !i,也就是为了对计算结果取反,如果x真的为-1,i就为0,!i一定为1。这样的值会对结果x + i产生干扰,从而作为条件返回。
Q4
/*
* allOddBits - return 1 if all odd-numbered bits in word set to 1
* where bits are numbered from 0 (least significant) to 31 (most significant)
* Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
int allOddBits(int x) {
/*首先获取0xAAAAAAAA作为此问题的掩码, 0xAA作为常数值是可以直接使用的*/
int i = (0xAA << 8) | 0xAA;
int j = (i << 16) | i; /*j == 0xAAAAAAAA*/
return !((x & j) ^ j); /*x & j保留了所有奇数位的值,与j异或可以看出是否与j相等*/
}
Q4分析:
这道题的关键在于找出掩码值0xAAAAAAAA,这个数字很特别,它保留了所有奇数位。二进制展示如为:0b 10101010101010101010101010101010。解题思路就是:
1.先构造出这样一个掩码值;
2.使其与x相与来获取x奇数位的对应情况;
3.通过与掩码值异或来判断x对应奇数位是否全为1。
从这个题中大致可以发现这样一种思路引导,也就是当一个问题需要数字中某些特殊位时(比如本题是保留所有奇数位),下手点往往就是先找特殊掩码再进行判断。
Q5
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return (~x + 1);
}
Q5分析:
这道题是求一个数的逆元,也就是-x,简单总结一下求法:
1.CSAPP课本上的解法:
A.如果x是Tmin,那么x的逆元就是自身Tmin,即Tmin + Tmin = 0
B.如果x不是Tmin,那么x的逆元就是-x,因为这时的取值范围关于0是对称的
这是一种非常直白的理解方法,但是它对本题没有指导作用。
2.位运算解法:
对任何补码,-x = (~x + 1),逆元的位的快速求解就是按位取反再加1,这是一个非常重要的结论。
Q6
//3
/*
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
int isAsciiDigit(int x) {
int j = !(x ^ 0x38); /*特殊值0x38*/
int k = !(x ^ 0x39); /*特殊值0x39*/
/*
0 0 0 0
0 0 0 1
0 0 1 0
0 0 1 1
0 1 0 0
0 1 0 1
0 1 1 0
0 1 1 1
只需要保证第三位为0
*/
x = !((x >> 3) ^ 0x6);
return x | j | k;
}
这道题使用位运算来判断某个值是否属于特定范围。
如果将满足条件的数字的二进制位全部列出来,如下所示:
0011 | 0000 |
---|---|
0011 | 0001 |
0011 | 0010 |
0011 | 0011 |
0011 | 0100 |
0011 | 0101 |
0011 | 0110 |
0011 | 0111 |
0011 | 1000 |
0011 | 1001 |
Q6分析:
从表中可以清楚地看出,除了最后两行0x38,0x39以外,其他范围内的数值第3位均为0,且0~2位包含了所有可能的情况。这意味着它们可以被归并为一类,而将剩下的两个作为特例来处理,变量j、k就分别验证了0x38、0x39这两个特例的情况,取反是为了将其当作条件来使用。剩下的就是将原值x右移三位,此时应该确保剩下来的值是0x6(110),这对应了剩下的情况。最终返回值为三个条件的或,意思是满足了其中一条即可。
Q7
/*
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
int conditional(int x, int y, int z) {
/*注意异或运算的一个性质,x ^ y ^ y = x*/
int i = !(x ^ 0); /*测试x是否是0*/
int m = ~(i) + 1; /*求逆元,1的时候返回-1,0的时候还返回0*/
int n = ~(!i) + 1; /*与-1相与时会保留原值,与0相与会得到0*/
return y ^ z ^ (m & y) ^ (n & z);
}
Q7分析:
一道颇有趣味的题,上面是我自己的解法。这道题在不允许使用条件判断的情况下实现选择,确实颇费头脑。主要的性质是异或运算的一个性质,即x ^ y ^ y = x,一个数与同一个数连续异或两次,会再次得到自身,有时候也会使用此性质进行加密,其中y称为密钥。
回到这道题,我的想法就是直接将y和z异或起来,通过条件来控制解密的密钥到底是y还是z,从而对应的实现将y或z屏蔽。首先使用一个变量i将x是否为0的条件判断结果存储起来,然后直接使用m、n分别对i,-i进行取反加一,也就是直接取其逆元。因为i和-i总有一个是1,另一个是0,1的逆元是-1,0的逆元是0,任何数与-1相与总能得到自身,而与0相与会变成全0。至此,我们有了结果的基础。
最终返回结果为y ^ z ^ (m & y) ^ (n & z),自己举例测试就可以知道,m&y与n&z总有一个为0,另一个就是想要的掩码。
Q8
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
int sx = !((x >> 31) + 1); /*为0表示符号位为0,为1表示符号位为1*/
int sy = !((y >> 31) + 1);
/*情况1.1:异号 sx = 1, sy = 0*/
int c1 = (sx & (!sy));
/*情况1.2:异号 sx = 0, sy = 1,这个不可以成立*/
int c2 = ((!sx) & sy);
/*情况2:同号,用x - y是否<=0判断*/
int i = (~y) + 1; /*求出y的逆元*/
int j = x + i; /*x + (-y) = x - y*/
/*接下来判断j是否小于等于0*/
int m = !j; /*如果j == 0, m一定为1*/
int n = !((j >> 31) + 1); /*如果j < 0,那么n一定为1*/
return c1 | ((!c2) & (m | n));
}
Q8分析:
这道题要求使用指定运算符完成是否小于等于的判断,我是通过分类讨论来做的,也就是对符号位进行讨论。在x y同号时(同大于0或同小于0时),我们可以放心的进行x-y操作,这样确保不会因为溢出而导致结果的符号错误,因此这两种情况可以合并为一种情况,即情况2。另外两种情况就是异号的情况,这两种情况下大小是容易判断的。
sx | sy | 结果 |
---|---|---|
0 | 0 | 情况2,同号,使用x-y是否小于等于0来判断 |
0 | 1 | 情况1.2,异号,x > y一定成立 |
1 | 0 | 情况1.1,异号,x < y一定成立 |
1 | 1 | 情况2,同号,使用x-y是否小于等于0来判断 |
具体到代码实现时,c1即代表着情况1.1(x < 0, y > 0),c2代表着情况1.2(x > 0, y < 0)。显然c1是符合条件的,c2不符合条件但它却是统一讨论情况2的障碍,所以最终在返回条件中一定要有!c2来将情况1.2排除掉,再与两种同号条件相与。
Q9
//4
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int logicalNeg(int x) {
int nx = (~x + 1); /*得到-x*/
/*如果x不为0,那么x与-x相或之后符号位一定为1*/
int i = x | nx;
/*只需要判断i最高位是否为1就可以了*/
return (i >> 31) + 1;
}
Q9分析:
这道题要求采用指定的运算符实现逻辑非,即对于0值返回1,非0值返回1。
要解决此问题,一个很重要也稍微困难的事情是要敏锐的察觉到一个条件,可以将0和非0值完全区分开来,这个条件就是:
对于任何一个非零数x,当x != Tmin时,x与-x的符号位一定是异号的,这样它们相或的结果一定可以保证符号位为1。特殊的,当x == Tmin时,x与-x均为Tmin,符号位均为1,相或结果也为1。这样我们证明了对于非零数x,-x | x一定可以保证符号位为1。而对于0,这样的结论是不成立的,这就找到了能够完全区分0值和非0值的一个清晰的边界。
接下来就是将上述思路转换成代码了,问题描述很简单,其实解决过程还是有些难度的。
Q10
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int howManyBits(int x) {
/*获取数字的符号位, 如果符号位为1,那么Sign也是1*/
int Sign = x >> 31;
/*如果是负数那么按位取反,如果是正数那么保持原值*/
x = (Sign & ~x) | (~Sign & x);
/*接下来就是寻找数字中最高位的1在哪里*/
/*
以第一个式子为例,如果判断高16为仍有1存在,
那么低16位必须保留,不用考虑它们了,直接移掉
*/
int High16 = !!(x >> 16) << 4;
x = x >> High16;
int High8 = !!(x >> 8) << 3;
x = x >> High8;
int High4 = !!(x >> 4) << 2;
x = x >> High4;
int High2 = !!(x >> 2) << 1;
x = x >> High2;
int High1 = !!(x >> 1);
x = x >> High1;
int High0 = x;
return High16 + High8 + High4 + High2 + High1 + High0 + 1;
}
Q10分析:
这题没有做出来,到网上找的题解,简单阅读了一下代码之后,发现确实很巧妙。首先要搞清楚基本思路:要搞清楚一个数补码最少需要多少位,必须得找出来正数中位于最高位的1在哪里,这一点很好理解。如果是负数,就要找负数中最高位的0在哪里,然后再+1(表示符号位)。
这一点比较难想,大概可以这样理解,负数的负权重全部是由最高符号位贡献的,越大的负数越容易使用较小的位数表示,如果我们想表示更小的负数就必须增加位数,同时保证符号位为1,在负数的二进制表示中出现的最高位0是一种标志,意思是这里的正权重不用了,那么也可以确定表示此数无需更多的二进制位。下面开始解释代码:
x = (Sign & ~x) | (~Sign & x);
上面这行代码将正数保持原值,负数按位取反,这样是为了将上面的两种情况结合起来。这时,后面的代码只需要寻找数字中最高位的1即可。后面就是连续的移位判断来判断数字中最高位的1在哪里出现。这里遵循的原理很简单,就是如果一个数位是1,那么比它低的位一定需要保留,所以我们干脆将低位全部移掉(反正它们是一定要保留的),以高16位为例,如果它不是全0,那么低16位一定要保留等等。之所以选2的幂次进行移位,就是因为这样的移动位数方便实现,不需要复杂的组合。
Q11
//float
/*
* floatScale2 - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatScale2(unsigned uf) {
/*提取此浮点数中的各部分*/
unsigned Sign = uf >> 31; /*符号位,注意无符号数执行逻辑右移*/
unsigned Exp = (uf >> 23) & 0xff; /*指数部分*/
unsigned Frac = uf & 0x7fffff; /*小数部分*/
/*判断parameter是否为NaN,即Exp全1且Frac不等于0*/
if(Exp == 0xff && Frac != 0)
return uf;
/*返回值*/
unsigned Result = 0;
if(Exp == 0) // 非规格化数
{
/*乘以2首先尝试将小数部分frac左移一位,查看是否有进位到exp*/
int Carry = (Frac << 1) >> 23;
Frac = (Frac << 1) & 0x7fffff; // 移位之后的Frac部分
Exp = Exp + Carry;
}
else if(Exp != 0xff) // 如果是规格化数且不是无穷大
Exp = Exp + 1;
/*将数字重组回去*/
Result = (Sign << 31) | (Exp << 23) | Frac;
return Result;
}
Q11分析:
可以稍微松一口气了,浮点数的第一题,因为放宽了编程限制,我们手头的可以使用的工具更多了,这题要求将给定的浮点数乘以2,但是操作是在一个32位的无符号整数上进行的。思路其实很简单,就是分类讨论:规格化数和非规格化数,规格化数里面NaN和无穷大的情况需要摘出来。
1.当数字是非规格化数字时,只需要将Frac部分左移一位即可,如果有进位,那么就实现了从非规格化数到规格化数的平滑过渡。
2.当数字是规格化数字时,因为Frac部分需要加上一个隐式的1,所以单凭左移一位无法获得对应的2倍关系,这时候只需要直接在Exp部分+1即可,但是要排除无穷大的情况,无穷大返回自身即可。NaN直接在进行运算之前直接剔除掉。
Q12
/*
1. floatFloat2Int - Return bit-level equivalent of expression (int) f
2. for floating point argument f.
3. Argument is passed as unsigned int, but
4. it is to be interpreted as the bit-level representation of a
5. single-precision floating point value.
6. Anything out of range (including NaN and infinity) should return
7. 0x80000000u.
8. Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
9. Max ops: 30
10. Rating: 4
*/
int floatFloat2Int(unsigned uf) {
int Result = 0x80000000;
/*抽取浮点数中的各部分*/
unsigned Sign = uf >> 31; /*符号位,注意无符号数执行逻辑右移*/
unsigned Exp = (uf >> 23) & 0xff; /*指数部分*/
unsigned Frac = uf & 0x7fffff; /*小数部分*/
unsigned Bias = 127; /*单精度浮点数的固有偏置*/
/*如果是NaN或者无穷大*/
if(Exp == 0xff)
return Result;
/*非规格化数取整一定是0*/
if(Exp == 0)
{
Result = 0;
return Result;
}
/*规格化数的情况*/
else
{
/*观察还需要左移多少位,负数则右移*/
int Difference = Exp - Bias - 23;
int Bound = 7; /*左移超过7位就会out of bound*/
/*加上规格化数中隐含的1*/
Frac = Frac | (1 << 23);
if(Difference >= 0) /*左移*/
{
if(Difference <= Bound)
Frac = Frac << Difference;
else /*out of bound*/
return Result;
}
else /*右移*/
{
Difference = ~Difference + 1; /*取负*/
if(Difference < 24) /*最多24位有值*/
Frac = Frac >> Difference; /*右移*/
else
Frac = 0;
}
}
/*无论正负数,我们此时得到了浮点数的整数部分绝对值*/
Result = 0 | Frac;
if(Sign) /*如果浮点数是负数*/
Result = ~Result + 1;
return Result;
}
Q12分析:
这道题显得代码有点长(可能是我的问题),但是它的解决思路并不难。这道题要求实现从单精度浮点数float到int的强制类型转换,我们只需要先求出浮点数的整数部分,放到一个整数int的0~30位即可(因为MSB是符号位)。所以需要分几种情况讨论一下:
1.首先是infinity或NaN,这种情况下直接返回要求的值0x80000000即可。
2.非规格化数取整一定是0,也很简单
3.规格化数稍微复杂,解答思路就是无论正负浮点数,如果在int表示范围内,那么我们保留其绝对值。负数在最后进行取反即可,在处理时注意越界问题,考虑到int只有31位可以用,而规格化数加上隐含的1和Frac部分已经有24位,故左移超过7位就会out of bound,右移超过24位一定会得0。
Q13
/*
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
*
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return
* 0. If too large, return +INF.
*
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatPower2(int x) {
unsigned Result = 0;
/*如果数字很小,超过了最小的非规格化数*/
if(x < (1 - 127 - 23))
return Result;
/*较小的数字,但尚可以非规格化数来表示, 2 to the -126是第一个规格化数*/
else if(x >= (1 - 127 - 23) && x < -126)
{
/*计算Frac部分的1应该在哪一位*/
unsigned Difference = -126 - x;
Result = 1 << (23 - 1 - Difference);
return Result;
}
/*可以使用规格化数表示的情况*/
else if(x >= -126 && x <= 254 - 127 + 1)
{
/*转换x为对应的Exp部分*/
x = x + 127;
Result = x << 23;
}
/*超过了最大的浮点数*/
else
Result = 0xff << 23;
return Result;
}
Q13分析:
这道题要求求2的x次幂的浮点数表示,关键在于找准每种情况下(规格化数、非规格化数)的表示范围:
- 小于了最小的非规格化数,最小的非规格化数为0 00000000 00000000000000000000001,也就是2的-149次幂,小于这个数返回0。
- 在非规格化数的表示范围内,那就是大于等于-149次幂,但是小于最小的规格化数即2的-126次幂。
- 在规格化数的表示范围内,就是大于等于-126次幂,小于等0 11111110 11111111111111111111111,即2的+128次幂。因为是2的整数次幂,所以可以知道Frac部分一定全部是0,只需要求Exp部分即可。
- 超过了最大的规格化数,这时候就返回infinity即可。
终于做完了,data lab作为csapp的实验,还真是很难。这些问题中Q10不是自己做出来的,感觉很遗憾。这些问题的解决思路也真是非常不寻常,很多与日常的编程思维相差甚远,不过能够从位的角度来认识和学习计算机中数字的表示,也算是一种独特的体验,就把这些题当作一些智力游戏吧。