《CSAPP》号称程序员圣经,虽然中文译名为《深刻理解计算机系统》,但其实没那么“深”,只是覆盖面很广,通常用做计算机专业大一导论课的教科书。早就听闻书上配套的实验十分经典,此次重温新版(第三版),打算把全部的实验都作一下,也写个系列博文,好记录实验过程。实验能够在书本配套网站CSAPP: Lab Assignments下载,这篇从第一个实验 —— 位操做开始。html
本实验是第二章《信息的表示与处理》的配套实验,要求使用一个高度限制的C语言子集实现一些特定的逻辑,整数,浮点数的函数。延用第一章的说法,信息就是位加上下文,计算机系统中全部的信息都是由一串比特(或者说一串二进制数字)表示的,第二章就讲了C语言中整数与浮点数的编码方式,即典型地,计算机是如何用一串比特来表示整数与浮点数的:程序员
一样从内存里取出4个字节 \(0x80000000\) ,把它当无符号整数看,就是 \(2147483648\);把它当有符号整数看,就是 \(-2147483648\);把它当单精度浮点数看,就是 \(-0\)。所谓上下文,就是解读这串比特的方式,横当作岭侧成峰。值得注意的是,尽管在几乎全部系统上,C语言整数与浮点数都是这么编码的,但C语言标准自己并无这样规定,不知道有生之年能不能赶上非主流的编码方式。
若是没有彻底掌握这些数字的编码方式以及C语言的位操做,是必定没法完成实验一的。实验一好就好在会让你反复回忆这些基础知识,深刻细节之中,作完实验后想忘都忘不了:)express
尽管有C语言有标准,但Undefined Behavior仍是太多,尤为是深刻底层进行位操做的状况下,所以实验预设: 有符号整数使用32位二进制补码编码; 右移操做为算术位移,高位补符号位。实验还要求:不能使用宏;整数操做不能使用大于0xFF的常量。下面就逐个函数记录实验过程了。app
用~
和|
实现&
,有公式很简单,但记不住,用韦恩图辅助思考:全集表示全部位都为1,x
与y
分别表示特定位置为1的子集,想象一下~
,|
和&
的韦恩图,一会儿就推出公式来了。less
/* * 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); }
x
右移 n * 8
位,取最后一个字节便可,利用了n * 8 == n << 3
。函数
/* * 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; }
实验预设了右移为算术位移,那么对x
右移n
位再用掩码将高位补的n
位置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 mask = ~(((1 << 31) >> n) << 1); return (x >> n) & mask; }
这题想了好久,正常的想法是将x
一位一位地右移,用掩码1取最低位,再求和,然而操做符数量超标:D 而后想到,用x & 1
去检查x
最后一位是不是1比较亏,能够用x & 0x00010001
,这样能够一次检查两位,最后将先后16位的结果汇总便可,然而操做符数量仍是超标:D最终将x
分了8组,x & 0x11111111
,每次检查8位,用了38个操做符,终于达标。这是全部题目中用的操做符数量最多的一题了。ui
/* * 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 mask = 0x11 + (0x11 << 8) + (0x11 << 16) + (0x11 << 24); int count = (x & mask) + ((x >> 1) & mask) + ((x >> 2) & mask) + ((x >> 3) & mask); return (count & 7) + ((count >> 4) & 7) + ((count >> 8) & 7) + ((count >> 12) & 7) + ((count >> 16) & 7) + ((count >> 20) & 7) + ((count >> 24) & 7) + ((count >> 28) & 7); }
一开始想在0上面做文章,毕竟只有bang(0) = 1
,但此路不通。|
操做,二分法,逐渐把高位的1收集到低位,如x = x | (x >> 16)
,若是高位的16位有1的话,就会被收集到低位的16位上,依此二分,收集到最后一位,恰好12个操做符。编码
/* * bang - Compute !x without using ! * Examples: bang(3) = 0, bang(0) = 1 * Legal ops: ~ & ^ | + << >> * Max ops: 12` * Rating: 4 */ int bang(int x) { x = x | (x >> 16); x = x | (x >> 8); x = x | (x >> 4); x = x | (x >> 2); x = x | (x >> 1); return ~x & 1; }
最简单的一题,要熟悉二进制补码。spa
/* * tmin - return minimum two's complement integer * Legal ops: ! ~ & ^ | + << >> * Max ops: 4 * Rating: 1 */ int tmin(void) { return 1 << 31; }
若x
非负,考虑到n
位二进制补码能表示的最大非负数为 $0b0111...111 $ (共n-1
个1),用掩码将x
低位的n-1
位置0,检查高位的32 - (n - 1)
位是否为0便可。若x
为负,先将其转为非负数~x
,编码~x
必需的位数与编码x
的是相同的。
/* * 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 minusOne = ~0; int mask = minusOne << (n + minusOne); return !((x ^ (x >> 31)) & mask); }
x >> n
即为\(\lfloor x / 2^n \rfloor\),结果是向下取整的,但题目要求向0取整,若x
非负向下取整便是向0取整没有问题,若x
为负,须要向x
加上一个偏移值\(2^n - 1\),使得x >> 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 signBit = (x >> 31) & 1; int bias = (signBit << n) + (~signBit + 1); return (x + bias) >> n; }
n位二进制补码的值域是\([-2^{n-1},\ 2^{n-1} - 1]\),并不关于0对称,所以当x
为最小值时-x
是它本身。
/* * negate - return -x * Example: negate(1) = -1. * Legal ops: ! ~ & ^ | + << >> * Max ops: 5 * Rating: 2 */ int negate(int x) { return ~x + 1; }
正数的符号位为0,0的符号位也是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) & (!(x >> 31)); }
isLessOrEqual
等价于!isGreater
,实现isGreater
简单点:若x
y
异号,则x
必须非负y
必须为负;若x
y
同号,x - y
不会溢出,必有x - y > 0
,即x - y - 1 >= 0
,即x + ~y >= 0
,检查x + ~y
的符号位便可。
/* * 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 xSign = x >> 31; int ySign = y >> 31; int hasSameSign = !(xSign ^ ySign); int diffSign = (x + ~y) >> 31; int isXPosYNeg = (!xSign) & ySign; int isGreater = isXPosYNeg | (hasSameSign & !diffSign); return !isGreater; }
这道题容许90个操做符,是全部题目对操做符数量最宽松的了。ilog2
的实质是求x
最高位的1的索引,若x
高位的16位有1,则不用管低位的16位;若x
高位的8位有1,则不用管低位的24位,依次类推。实现起来仍是十分巧妙的:)
/* * 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 high16, high8, high4, high2, high1; high16 = (!!(x >> 16)) << 4; x = x >> high16; high8 = (!!(x >> 8)) << 3; x = x >> high8; high4 = (!!(x >> 4) << 2); x = x >> high4; high2 = (!!(x >> 2) << 1); x = x >> high2; high1 = !!(x >> 1); return high1 + high2 + high4 + high8 + high16; }
终于到浮点数了,浮点数的题对操做符要求宽松一点,还能够用循环跟判断语句。第一题,只要对IEEE754熟悉就好了。
/* * 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) { int isNaN = (((uf >> 23) & 0xFF) == 0xFF) && (uf << 9); return isNaN ? uf : ((1 << 31) ^ uf); }
没什么技巧,十分暴力。从符号位,阶码,尾数,舍入,一个一个来。注意,float(x)
是向偶数取整的。
/* * 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) { unsigned sign = x & (1 << 31); unsigned exp = 0; unsigned frac = 0; unsigned round = 0; unsigned absX = sign ? (~x + 1) : x; unsigned tmp = absX; while ((tmp = tmp >> 1)) ++exp; frac = absX << (31 - exp) << 1; round = frac << 23 >> 23; frac = frac >> 9; if (round > 0x100) round = 1; else if (round < 0x100) round = 0; else round = frac & 1; return x ? (sign | ((exp + 0x7F) << 23) | frac) + round : 0; }
仍是很暴力,按照浮点数分类一个一个来:特殊值,直接返回;规范化的浮点数,阶码加1;非规范化的,左移一位并保持符号位不变。
/* * 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) { unsigned sign = 1 << 31; unsigned isNormalized = uf << 1 >> 24; unsigned isSpecial = isNormalized == 0xFF; if (isSpecial || uf == 0 || uf == sign) return uf; if (isNormalized) return uf + (1 << 23); return (uf << 1) | (uf & sign); }