1. 任务内容
通过本次 lab 熟悉整数及浮点数的位表示方式,解开一些 puzzle。
puzzle 主要分为三种类型:位操作、整型运算和浮点数运算。
名称 |
描述 |
难度 |
指令数目 |
bitAnd(x,y) |
只用和 ~ 实现 x & y |
1 |
8 |
getByte(x,n) |
从数 x 中提取第 n 个字节 |
2 |
6 |
logicalShift(x,n) |
实现逻辑右移 |
3 |
20 |
bitCount(x) |
计算二进制数 x 中 1 的个数 |
4 |
40 |
bang(x) |
不使用!而计算!x |
4 |
12 |
tmin() |
返回所能表示的最小整数 |
1 |
4 |
fitsBits(x,n) |
如果x可以表示为n位二进制补码形式,则返回1 |
2 |
15 |
divpwr2(x,n) |
计算x/(2^n) |
2 |
15 |
negate(x) |
计算 -x |
2 |
5 |
isPositive(x) |
x > 0 返回1,x <= 0返回0 |
3 |
8 |
isLessOrEqual(x,y) |
x <= y 返回 1 否则返回0 |
3 |
24 |
ilog2(x) |
返回不超过以2为底x对数的最小整数 |
4 |
90 |
float_neg(uf) |
返回和浮点数参数-f相等的二进制数,返回参数本身当参数是NaN时 |
2 |
10 |
float_i2f(x) |
实现(float)x,返回x对应浮点数的二进制表示形式 |
4 |
30 |
float_twice(uf) |
返回以unsinged表示的浮点数二倍的二进制形式 |
4 |
30 |
2. 上手指南
一共 13 个需要补充的函数,所有的工作都只需修改 bits.c 文件。
任务指示说明
- 整型的范围是0~255(0xff)
- 只能包含参数和局部变量,不能使用全局变量
- 单目运算符:! ~
- 双目运算符:& ^ | + << >>
不允许的操作
- 使用任何条件控制语句
- 定义或使用宏
- 定义其他的函数
- 调用函数
- 使用其他的操作符
- 使用类型转换
- 使用除 int 之外的类型(针对整型)
- 使用除 int, unsigned 之外的类型(针对浮点数)
可以认为机器:
- 使用 2’s complent,32位
- 执行算术右移(左端补k个有效位)
- 移动超过字长的位数会出问题
测试代码
- 运行
make btest
编译函数
- 使用
dlc compiler (./dlc)
自动检测你的代码是否符合规定
- 运行
./btest
检测函数是否编写成功
- 使用
./ishow n
查看n的十六进制,有符号整型和无符号整型形式
- 使用
./fshow n
查看n的浮点数表示形式
3. 题目及解法
bitAnd
只用和 ~ 实现 x & y
1 2 3
| int bitAnd(int x, int y) { return ~((~x) | (~y)); }
|
摩尔定律 x & y = ~(~(x & y))
getByte
从数 x 中提取第 n 个字节
1 2 3 4 5
| int getByte(int x, int n) { n = n << 3; //n * 8 x = (x >> n) & 0xff; // 右移n位 return x; }
|
logicalShift
实现逻辑右移
1 2 3 4 5
| int logicalShift(int x, int n) { int mask; mask = ~((1 << 31) >> n << 1); return (x >> n) & mask; //x右移n位,前n位置0 }
|
逻辑右移要在原最高位前 n 位置零
bitCount
计算二进制数 x 中 1 的个数
方法一: 采用分治法,把每一位都当成独立的数,并求和。参考链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| int bitCount(int x) { int tmp, mask1, mask2, mask3, mask4, mask5, sum; // mask1: 0x55555555 tmp = 0x55 | (0x55 << 8); mask1 = tmp | (tmp << 16); // mask2: 0x33333333 tmp = 0x33 | (0x33 << 8); mask2 = tmp | (tmp << 16); // mask3: 0x0f0f0f0f tmp = 0x0f | (0x0f << 8); mask3 = tmp | (tmp << 16); // mask4: 0x00ff00ff mask4 = 0xFF | (0xFF << 16); // mask5: 0x0000ffff tmp = 0xFF | (0xFF << 8); mask5 = tmp | (0 << 16); sum = (x & mask1) + ((x >> 1) & mask1); sum = (sum & mask2) + ((sum >> 2) & mask2); sum = (sum & mask3) + ((sum >> 4) & mask3); sum = (sum & mask4) + ((sum >> 8) & mask4); sum = (sum & mask5) + ((sum >> 16) & mask5); return sum; }
|
- 每1位视为一个数,相邻求和,结果用2位保存。
- 每2位视为一个数,相邻求和,结果用4位保存。
- 每4位视为一个数,相邻求和,结果用8位保存。
- 每8位视为一个数,相邻求和,结果用16位保存。
- 右移16位:每16位视为一个数,相邻求和,结果用32位保存。即为最终结果。
为什么要右移呢?以每4位视为一数为例,1001被视为两个数10,01,若要将二者求和,则需要分别将二者从原数中取出,然后相加。掩码0011可以直接取出01,若要取出10,则需要先将10右移到低位。
1 2 3 4 5
| So if I have number 395 in binary 0000000110001011 (0 0 0 0 0 0 0 1 1 0 0 0 1 0 1 1) After the first step I have: 0000000101000110 (0+0 0+0 0+0 0+1 1+0 0+0 1+0 1+1) = 00 00 00 01 01 00 01 10 In the second step I have: 0000000100010011 ( 00+00 00+01 01+00 01+10 ) = 0000 0001 0001 0011 In the fourth step I have: 0000000100000100 ( 0000+0001 0001+0011 ) = 00000001 00000100 In the last step I have: 0000000000000101 ( 00000001+00000100 )
|
方法二:也是分治法的思想,首先将32位数分为8组,对于每四位数,通过三次移位运算统计每组数中1的个数,然后将前16位与后16位相加,将1的个数浓缩在16位中,再以同样的方法将1的个数整理到8位中,得到最后结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| int bitCount(int x) { int sum = 0; // i = 0x11111111 int i = 0x11 | (0x11 << 8); i = i | (i << 16); //对于每四位,通过不停的移位运算将前三位的1加到第四位上 sum += x & i; sum += (x >> 1) & i; sum += (x >> 2) & i; sum += (x >> 3) & i; //令i = 0xffff; i = 0xff | (0xff<<8); //将前16位与后16位相加 sum = (sum >> 16) + (i & sum); //令i = 0x0f0f //整理每8位之和 i = 0x0f | (0x0f<<8); sum = ((sum >> 4) & i) + (sum & i); //将前8位与后8位相加 i = 0xff; sum = (sum >> 8) + (i & sum); return sum; }
|
bang
不使用!而计算!x
1 2 3 4 5
| int bang(int x) { int i = ~x + 1; int j = (i | x) >> 31; //符号相同则为0,不同为1 return j + 1; }
|
零的特殊之处,零和零的相反数首位都是零,-0 = +0 = 0,-x只有在x=0时符号位才不会改变。故只需利用x | (-x) = x | (~x + 1)的符号位即可
tmin
返回所能表示的最小整数
1 2 3 4
| int tmin(void) { return 0x80 << 24; // 0x80000000 // return 1 << 31; }
|
fitsBits
如果x可以表示为n位二进制补码形式,则返回1
1 2 3 4 5
| int fitsBits(int x, int n) { int i = 32 + (~n + 1); int a = (x << i) >> i; return !(x ^ a); }
|
左移32-n位,右移32-n位,若不变则 return 1
divpwr2
计算x/(2^n)
1 2 3 4
| int divpwr2(int x, int n) { int bias = (1 << n) + ~0; return (x + ((x >> 31) & bias)) >> n; }
|
学习CSAPP第三版 2.3.7内容可知:
无论x正负,结果都是舍入到0。因此正数向下舍入,负数向上舍入。直接右移n位会导致不论正负数都是向下舍入。
当 x 为正数时,直接 x >> n 即可。当 x 为负数时,直接右移会同样向下舍入(前提是补码与无符号数的乘法表示形式相同),所以要增加一个偏移量,然后再右移,使其向上舍入。
negate
计算 -x
1 2 3
| int negate(int x) { return ~x + 1; }
|
按位取反加一得到相反数。
关于2的补码可以看这里
isPositive
x > 0 返回1,x <= 0返回0
1 2 3 4 5
| int isPositive(int x) { int m = (x >> 31) & 1; // 取符号位 int i = !!x; // 判断是否为 0 return i ^ m; }
|
isLessOrEqual
x <= y 返回 1 否则返回0
return 1 : x< 0,y > 0 or x - y <= 0
1 2 3 4 5 6 7 8
| int isLessOrEqual(int x, int y) { int m = (~x + 1) + y; //y-x int p = !!(x >> 31); //x的符号位 int q = !!(y >> 31); //y的符号位 int i = !(m >> 31); //y-x>=0时i==1 int j = ! (p ^ q); // 判断是否同号 return (i & j) | (p & !q); }
|
假如直接用x > y => x - y > 0 => x + (~y + 1) > 0
这个性质,在-x = x(x = 0x80000000)时会被截断,因为计算机并不是纯数学,加法与可能会溢出。
ilog2
返回不超过以2为底x对数的最小整数。找最高位的1出现的位置,也就是求最高有效位的右边有多少位。
解法一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int ilog2(int x) { int mark; // 先判断高16位是否有1,出现1则确定最终结果数量级必然大于16 mark = (!!(x >> 16)) << 4; // 判断高8位,一次类推 mark += ((!!(x >> (mark + 8))) << 3); mark += ((!!(x >> (mark + 4))) << 2); mark += ((!!(x >> (mark + 2))) << 1); mark += ((!!(x >> (mark + 1))) << 0); mark += (!!mark) + (~0) + (!(1 ^ x)); //考虑x=0 return mark; }
|
解法二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| int ilog2(int x) { int tmp, mask1, mask2, mask3, mask4, mask5, v; x = x | (x >> 1); x = x | (x >> 2); x = x | (x >> 4); x = x | (x >> 8); x = x | (x >> 16); // bitCount tmp = 0x55 | (0x55 << 8); mask1 = tmp | (tmp << 16); // 0x55555555 tmp = 0x33 | (0x33 << 8); mask2 = tmp | (tmp << 16); // 0x33333333 tmp = 0x0F | (0x0F << 8); mask3 = tmp | (tmp << 16); // 0x0F0F0F0F mask4 = 0xFF | (0xFF << 16); // 0x00FF00FF tmp = 0xFF | (0xFF << 8); mask5 = tmp | (0 << 16); // 0x0000FFFF v = (x & mask1) + ((x >> 1) & mask1); v = (v & mask2) + ((v >> 2) & mask2); v = (v & mask3) + ((v >> 4) & mask3); v = (v & mask4) + ((v >> 8) & mask4); v = (v & mask5) + ((v >> 16) & mask5); return v + ~0; // x - 1 }
|
先把有效位右边的所有位都置1,然后计算1的个数。
怎么把所有有效位都置为1呢?把最高有效位不停地移到右边呗。先右移1位,得到11,然后右移2位,得到1111,依此类推。例如 0010 1011 => 0011 1111
float_neg
返回和浮点数参数-f相等的二进制数,返回参数本身当参数是NaN时
1 2 3 4 5 6 7 8 9 10 11 12 13
| unsigned float_neg(unsigned uf) { unsigned result; unsigned m; // 最高位取反 result = uf ^ 0x80000000; // 最高位清 0 m = uf & 0x7fffffff; if (m > 0x7f800000) { result = uf; } return result; }
|
NaN 时返回 NaN,否则改变符号位
float_i2f
实现(float)x,返回x对应的浮点数的32位754标准的二进制存储格式。
此题考查对于IEEE二进制浮点数算术标准(IEEE 754)的掌握
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| unsigned float_i2f(int x) { // s 符号位,exp 最高位的位置即指数,franc 记录尾数 unsigned s = 0, exp = 31, frac = 0, d = 0; if (x == 0) return 0; // 判断正负 if (x & 0x80000000) { s = 0x80000000; x = -x; } while (1) { if (x & 0x80000000) break; exp -= 1; //exp记录最高位的位置 x <<= 1; } if ((x & 0x000001ff) == 0x180) d = 1; //最后舍掉的8位若最高位为1且低七位仍有数,要进位 else if ((x & 0xff) > 0x80) d = 1; frac = ((x & 0x7fffffffu) >> 8) + d; //franc记录尾数 return s + ((exp + 127) << 23) + frac; }
|
float_twice
返回以unsinged表示的浮点数的二倍的二进制形式
1 2 3 4 5 6 7
| unsigned float_twice(unsigned uf) { if((uf & 0x7F800000) == 0) // 若是指数为0情况,非规格化 return (uf << 1) | (0x80000000 & uf);//特殊情况0x80000000 else if((uf & 0x7fffffff) >= 0x7f800000) //若数为NaN return uf; return uf + 0x00800000; }
|
指数位全为0的为非规格化数,指数为1 - Bias,底数为0.f,f表示小数字段。因此加倍时直接左移。规格化的数加倍时指数+1。