前言
CSAPP 这本书可以说是程序员必读专业书籍了。这本书以一个 “Hello World” C语言程序为起点,通过跟踪整个"Hello World"程序的生命周期,来讲解系统为了执行该程序都做了哪些事情。就像书名一样,学习本书,能让我们对计算机系统有一个更深入的理解。
在学长的推荐下,我开始啃 CSAPP(《深入理解计算机系统》) 这本书,读了前两章,感觉难度还是不小的,但收获也是挺多的,尤其是 “计算机抽象等级” 和 “浮点数” 这两块知识,学了之后有种豁然开朗的感觉。 这本书还有一个特点就是配套的几个实验了,俗话说“纸上得来终觉浅”,配合着实验食用这本书,真是嘎嘣脆,事半功倍。
Lab1 对应的就是第二章的知识点,写起来挑战性还是很大的。
Lab1 Solution
Lab1 主要就是用 C 语言在一些操作限制的情况下实现题目要求。
主要的限制是操作符和操作数。还有一点要注意的就是变量的声明必须放在函数的最前面。
整个实验还是很有难度的,我们除了要对正数,浮点数二进制运算要非常熟悉外,还有很多 trick需要我们去思考,毕竟操作数是有限的,我们进行操作的时候要尽可能的“节约”。
每写完一道题都可以用 btest 测试一下,但是这个只能测试结果是否正确,操作是否合规无法测试。在所有题目都完成后,可以运行 Driver.pl 对程序进行一个完整的测试,如果能得到所有的分数,那么恭喜你,这个实验就算是圆满完成了。(当然如果想追求极致,可以尝试用更少的操作数来完成实验)
bitAnd
x&y = ~~(x&y) = (x|~y)
/*
* bitAnd - x&y using only ~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
return ~(~x | ~y);
}
getByte
将要取出的八位移动到最右端,并和 0xff 做且运算。
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
return (x >> (n << 3)) & 0xff;
}
logicalShift
先进行算数右移,再将补充的高位置为0。
/*
* logicalShift - shift x to the right by n, using a logical shift
* Can assume that 0 <= n <= 31
* Examples: logicalShift(0x87654321,4) = 0x08765432
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int logicalShift(int x, int n) {
int result = (x >> n) & (~(((1 << 31) >> n) << 1));
return result;
}
bitCount
这道题是难度最大的一道了。
我们需要用二分的思想,先两位两位地统计1的个数,并将答案存在这两位上。然后四位四位地统计,接着是八位,直到统计完三十二位。
统计的时候运用错位相加的方法,需要注意的是在移位之前要将无关位置0,以免无关位相加对结果造成影响。
/*
* bitCount - returns count of number of 1's in word
* Examples: bitCount(5) = 2, bitCount(7) = 3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 40
* Rating: 4
*/
int bitCount(int x) {
int tmp = 0x55 | (0x55 << 8);
int mask1 = tmp | (tmp << 16);
int tmp1 = 0x33 | (0x33 << 8);
int mask2 = tmp1 | (tmp1 << 16);
int tmp2 = 0x0f | (0x0f << 8);
int mask3 = tmp2 | (tmp2 << 16);
x = (x & mask1) + ((x >> 1) & mask1);
x = (x & mask2) + ((x >> 2) & mask2);
x = (x & mask3) + ((x >> 4) & mask3);
x = x + (x >> 8);
x = x + (x >> 16);
return x & 0x3f;
}
bang
如果x是0的话,0|0的符号位仍为0;x非0的话,x|x 的符号位为1。
/*
* bang - Compute !x without using !
* Examples: bang(3) = 0, bang(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int bang(int x) {
int minusx = ~x + 1;
int sign = (x | minusx) >> 31;
return (~sign) & 0x1;
}
tmin
久违的送分题。
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
return 1 << 31;
}
fitsBits
这道题需要注意的就是符号位了。之所以向移动 (32-n) 位,而不是 (31-n) 位就是为了留出符号位。
/*
* fitsBits - return 1 if x can be represented as an
* n-bit, two's complement integer.
* 1 <= n <= 32
* Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*/
int fitsBits(int x, int n) {
int shiftNumber = 32 + (~n + 1);
return !((x << shiftNumber >> shiftNumber) ^ x);
}
divpw2
这题难点在于舍入。正数直接右移 n 位即可,负数则需要加一个偏移量再右移。
还有一点需要注意的就是掩码的求取了,需要一些技巧,毕竟操作数是有限的,还要节约使用。
/*
* divpwr2 - Compute x/(2^n), for 0 <= n <= 30
* Round toward zero
* Examples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*/
int divpwr2(int x, int n) {
int signx = x >> 31;
int mask = (1 << n) + (~0);
int bias = signx & mask;
return (x + bias) >> n;
}
negate
送分题。
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return ~x + 1;
}
isPositive
这道题难点在于0的判断。0有一个非常特殊的性质就是 !0 = 1,利用0的这个特点,此题就不难解出了。
/*
* isPositive - return 1 if x > 0, return 0 otherwise
* Example: isPositive(-1) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 8
* Rating: 3
*/
int isPositive(int x) {
return !((x >> 31) | (!x));
}
isLessOrEqual
这道题相当于枚举 x<=y 的所有情况:
- x为负数,y 为正数
- x 等于 y
- x 和 y 符号相同,并且 y-x 为正数
/*
* 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 signx = (x>>31) & 0x1;
int signy = (y>>31) & 0x1;
int tmp = signx ^ signy;
int tmp1 = tmp & signx;
int y_minus_x = y+(~x+1);
return tmp1 | (!(x^y)) | (!tmp & !(y_minus_x&(1 << 31)));
}
ilgo2
求以2为底,x 的对数。
这道题算是难题之一了,其原理和 bitCount 有些类似,都是用二分的思想,但这道题是求出最高位的1。
通过二分整数的所有位,找出最高位1的位置。
/*
* ilog2 - return floor(log base 2 of x), where x > 0
* Example: ilog2(16) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int ilog2(int x) {
int bitsNumber = 0;
bitsNumber = (!!(x >> 16)) << 4;
bitsNumber += (!!(x >> (bitsNumber + 8)) << 3);
bitsNumber += (!!(x >> (bitsNumber + 4)) << 2);
bitsNumber += (!!(x >> (bitsNumber + 2)) << 1);
bitsNumber += (!!(x >> (bitsNumber + 1)));
return bitsNumber;
}
float_neg
从这道题开始就进入了浮点数的练习,这部分需要我们对 IEEE 浮点数编码先回顾一下。
这道题难点就在于 NaN 的判断,如果 uf 是 NaN,需要原值返回。
/*
* float_neg - Return bit-level equivalent of expression -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 representations of
* single-precision floating point values.
* When argument is NaN, return argument.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 10
* Rating: 2
*/
unsigned float_neg(unsigned uf) {
unsigned result = uf ^ (1 << 31);
int tmp = 0xff << 23;
if((uf & (tmp|(0xff << 15)|(0xff << 7)|0xff)) > tmp){
result = uf;
}
return result;
}
float_i2f
这道题完全就是考察队 IEEE 浮点数编码的熟悉程度了。第一遍写的时候怎么都过不了,后来发现是望考虑舍入了。除此之外这道题操作数也是一个难点,需要用到不少小 trick 来精简操作数。
/*
* float_i2f - Return bit-level equivalent of expression (float) x
* Result is returned as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point values.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned float_i2f(int x) {
int first1 = 31;
int frac = 0;
int flag = 0;
unsigned tmp = 0;
int exp = 0;
int mask = (0xff << 15)|(0xff << 7)|(0x7f);
int mask1 = 1 << 31;
int sign = x & mask1;
if(x < 0){
x = -x;
}
if(x == 0)
return 0;
if(x > 0){
while(!(x >> first1)){
first1 = first1 - 1;
}
if(first1 > 23){
frac = (x >> (first1 - 23)) & mask;
tmp = x << (55 - first1);
if(tmp > mask1)
flag = 1;
else if(tmp == mask1){
if(frac & 0x1)
flag = 1;
}
}
else{
frac = (x << (23 - first1)) & mask;
}
}
exp = 127 + first1;
return sign + (exp << 23) + frac + flag;
}
float_twice
最后一道题是浮点数二倍乘。无穷大和 NaN 就直接返回原值就好,规格化数阶码直接加1,非规格化数尾数左移一位(移位后可能变为规格化数,但这并不影响答案,这也正是 IEEE 浮点数编码的巧妙之处,不用担心)。
/*
* float_twice - 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 float_twice(unsigned uf) {
int mask1 = 0xff << 23;
unsigned result = 0;
if((uf & mask1) == mask1){
result = uf;
}
else if((uf & mask1) == 0x0){
result = (uf & (1 << 31)) | (uf << 1);
}
else{
result = uf + (1 << 23);
}
return result;
}
后记
之前只知道浮点数有误差,但并不知其所以然。有些数,比如0.1是无法用浮点数准确表示的,只能近似表示。int转 float,long long 转 double 由于尾数限制,也都会被舍入。总的来说,浮点数的精度由尾数位数决定,范围是由阶码位数决定的。
CSAPP 这本书真的不错,无论是书上的知识还是实验的内容干货都非常的多。学了前两章,收获还是挺大的,尤其是整数和浮点数的二进制表示这部分知识。但是感觉进度实在是太慢了,按照现在的速度,今年都不一定把这本书看完= =,接下来还是提高效率,抓紧时间把这本书啃完吧。
后后记
书啃完了,实验做了四个,实验代码放在了 github 上,但是后面三个因为太懒都没写博客了 QAQ。之后学习工作要及时笔记,以便回顾。