准备把csapp详细看一遍,所有lab都做一遍,加深理解。
本篇是datalab的个人解法,所以很可能不是最优解。
原课程地址:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html
ok,现在就来一道道题说明。
0. 说明·
datalab着重于让学生理解 数字(integer,float point)在bit level上的表示与操作。通过限制学生的操作集(如仅能使用 ~, |, +等此操作),让学生在bit level上思考问题。
more detail:
对于Integer类型的题型:
- 所能使用的常量范围在 0 - 255之间;
- 只能使用函数参数和局部变量;
- 有限操作集, !,~, & ^ | + << >> (每道题有各自详细的操作集解释)
- 不能使用任何的 if, else, do, while, for, switch等
- 不能使用marco
- 不能调用函数
- 不能使用 && || - ?
- 不能使用类型转换
- 不能自定义任何类型(如struct, union, array等),实际上,所有题型都只能使用int
程序环境说明:
- 程序为32位程序,
- 右移操作默认为算数右移(也就是补充符号位)
对于float类型的题型:
可以使用, if, else, loop等。只能使用int, unsigned类型。
不能,定义宏,调用函数,类型转换,自定义类型,使用任何float类型的操作。
1. bitXor·
题目:
- 目标:实现x ^ y.
- 限制:只能使用操作符 ~, &
- 最大操作次数: 14
- 难度:1
解法:
这道题,我是从“数字电路”中的真值表去思考的,异或操作的真值表如下:
X | Y | Z |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
所以可列: $$ X \overline{Y} + \overline{X}Y = Z $$ 利用两次反,数不变原则,可变形为: $$ \overline{ (\overline{X\overline{Y}})(\overline{\overline{X}Y}) } = Z $$ 则可得:
1 | int bitXor(int x, int y) |
2. tmin·
题目:
- 目标:返回最小补码数 ,即0x80000000.
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数: 4
- 难度:1
解法:
这个题应该是最简单的了,没什么可说的。
1 | int tmin(void) |
3. isTmax·
题目:
- 目标:判断x是否是Tmax,如果是,返回1,否则返回0。
- 限制:只能使用操作符 ! ~ & ^ | +
- 最大操作次数: 10
- 难度:2
解法:
既然是要判断x == Tmax,那么就要思考Tmax的特殊性。 Tmax = 0x7f ff ff ff.
观察到一个性质, Tmax+1 = ~Tmax. 即 $$ 0X7fffffff+1 = \sim0X80000000 $$ 但是还得想一下,还有其它数,有相同的性质吗?
yes,的确有 , 0xff ff ff ff也满足这样的性质。
所以,现在问题需要加一个过滤,把0xff ff ff ff过滤掉。如何过滤呢?这就要思考 0x7f ff ff ff和0xff ff ff ff的不同了。
又观察到0xff ff ff ff+1 = 0,而0x7f ff ff ff + 1 = 0x80 00 00 00。 所以,可以利用这个特性,把0xff ff ff ff给过滤掉。 具体是采用 !! 运算。
对于!! 来说:
运算 | 原 | 运算后 |
---|---|---|
!! | 0 | 0 |
!! | 非0 | 1 |
所以有: !!(0x7f ff ff ff + 1) = 1, 而!!(0xff ff ff ff+1) = 0。
于是,代码为:
1 | int isTmax(int x) |
4. allOddBits·
题目:
- 目标:如果x的二进位的所有奇数位全位1,则返回1,否则返回0。 注:二进制最低位是第0位。
- 例子:allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
- 限制:只能使用操作符! ~ & ^ | + << >>
- 最大操作次数: 12
- 难度:2
解法:
这个题目难度不大,生成0xaa aa aa aa即可,然后判定是否相同即可(通过异或+! 可以判定)
1 | int allOddBits(int x) |
5. negate·
题目:
- 目标:返回-x
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数:5
- 难度:2
解法:
这题也很简单,取反+1即可,算是一个二进制的公式吧。只是要记住 -Tmin = Tmin
1 | int negate(int x) |
6. isAsciiDigit·
题目:
- 目标: return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters ‘0’ to ‘9’)
- 例子:
- isAsciiDigit(0x35) = 1.
- isAsciiDigit(0x3a) = 0.
- isAsciiDigit(0x05) = 0.
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数:15
- 难度:3
解法:
这道题的整体思路也比较简单,考虑一个更通用的方法, 如何判定 x<=y? 也就是 x- y <= 0. 即 x-y 的符号位为1即可(这里没有考虑负溢出问题)。
所以看代码:
1 | int isAsciiDigit(int x) |
7. conditional·
题目:
- 目标:实现三目运算符 same as x ? y : z
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数:16
- 难度:3
解法:
这道题其实颇为巧妙,同样用到了数电中的思想,注意,只是思想,实现起来是不一样的。 $$ RESULT = XY + \overline{X}{Z} $$ X=1,则RESULT=Y, X=0,RESULT = Z。
但是这只是在1bit的情况下,要换算到Integer(32bit)范围内,X要么等于0xff ff ff ff, 要么等于0x0.
问题转化为:
如果x = 0,则不做转化。
如果x= 非0, 则x要转换为0xff ff ff ff。
这里又要用到 **!!**运算了。
又观察到,
0 -1 = 0xff ff ff ff
1 -0 = 0
所以可写代码:
1 | int conditional(int x, int y, int z) |
8. isLessOrEqual·
题目:
- 目标:如果 x<=y ,则返回1, 否则返回0.
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数:24
- 难度:3
解法:
这道题目和前文的 isAsciiDigit
很相似,用的方法也是类似的,所以不赘述。
个人只是将运算按照 ”一、二、三、四象限“分成了四种情况考虑:
1 | int isLessOrEqual(int x, int y) |
9. logicalNeg·
题目:
- 目标:实现 ! 操作
- 例子: logicalNeg(3) = 0, logicalNeg(0) = 1
- 限制:只能使用操作符 ~ & ^ | + << >>
- 最大操作次数:12
- 难度:4
解法:
这里我的出发点是 0和其它数值有什么不同?
0的相反数是0. (注意Tmin的相反数也是Tmin)
所以设:
y = -x (前文有说-x的bit运算方式)
如果y的首位是0,则说明,x为0或Tmin。 既然0要映射为1,只要 y>>31 +1即可。
如果y的首位是1,则说明,x为非零非Tmin的数,这些值要映射为0,同样y>>31 +1 即可(注意是算数右移,所以y>>31 = 0xff ff ff ff)
现在,剩下的问题为如果区分0 和 Tmin。这个就很简单了。0的符号位为0,Tmin的符号位为1,再运用一次 x>>31+1即可。
所以,代码为:
1 | int logicalNeg(int x) |
10. howManyBits·
题目:
- 目标: 返回表示一个数(补码形式)所需要的最小bit数。
- 例子:
- howManyBits(12) = 5
- howManyBits(298) = 10
- howManyBits(-5) = 4
- howManyBits(0) = 1
- howManyBits(-1) = 1
- howManyBits(0x80000000) = 32
- 限制:只能使用操作符 ! ~ & ^ | + << >>
- 最大操作次数: 90
- 难度:4
解法:
这道题难度非常大。我认为是datalab中最难的一道题。如果能使用if,loop就比较简单,但是难就难在不能使用这些运算。
所以先说一下思路:
首先从正数出发,如果从最高位到最低位扫描,当你找到首个1时,此时的1所在bit位+1(加1是因为还要个符号位),即是所需要的符号位数量。当然了0是例外。
问题再稍作转化,首次扫描到两个相邻的bit位分别是0和1时,就算是找到了所需要的符号位数量。
再考虑一下负数,思考一下前面的黑体字,”扫描到两个bit位时0和1时,就算找到了“,那把负数的情况是不是就是 ”首次扫描到两个bit位分别为1和0时呢?“ (当然,-1除外)。
结合正负数,问题变为 从高位向低位扫描,首次扫描到两个相邻bit位值不同时,就算是找到了所需要的符号位数量,不过0和-1是特殊值。
前面说了这么多,其实并不是最终解法,前面说的目的为,“对于负数,我们可以用对待正数的相同的算法去处理”,即把负数翻转为正数即可(注意不是取相反数)。
先看下面代码:
关键代码1:
1 | // 如果 x< 0, 则翻转x |
到这里x一定是正数了。
现在就可以统一处理正负数了。问题是我们仍然无法通过if loop计算需要多少位bit来表示一个数。
为了更好的说明下面的算法,这里先贴一张图,以5bit数字进行说明:
现在的关键是如何得到这个 剩余3bit信息:
如果可以将当扫描位后的所有位置为1,我们就可以将问题转化为统计 当前数字的二进制表示 有多少个1了。(为什么要这么想,因为有bitcount算法啊)
关键代码2:
对于32位的数据,可以有:
1 | // 把最高位1后的所有位全部填充为 1 |
接下来就是bitcount算法:
问题已转化为统计二进制中有多个1了。
先以4bit的数字为例:如何统计 0 1 0 1的1的个数?
我们可以通过掩码+移位的方式获取:
0 1 0 1 & 1 = 1
(0 1 0 1 >> 1) & 1 = 0
(0 1 0 1 >> 2) & 1 = 1
(0 1 0 1 >> 3) & 1 = 0
最后 1 + 0 + 1 + 0 = 2。
转化为代码为:
1 | int sum = 0; |
ok,上面是4bit的情况,如何计算32位呢。 我们当然可以写32次 += 操作来做。但是有更聪明的做法,运用分治的思想,将一个32bit的数,分成4个8bit的段。对每个段,运用上面的算法运算即可。
具体需要个人分析一下代码了,代码很短,所以也很好想清楚。
具体代码:
1 | // 计算现在1的个数 分治算法 将整个二进制bit分成4段,每段8bit |
ok,现在的sum就是 32bit的二进制中的1的个数了。
结合上面所有算法,可得最终的代码:
1 | int howManyBits(int x) |
11. float_twice·
题目:
- 目标: 给定一个浮点数f,返回2*f的二进制表示(单精度float point)。如果为Nan,直接返回参数。
- 限制:只能使用int或unsigned,其余所有操作都可以使用。
- 最大操作次数: 30
- 难度:4
解法:
其实能用if loop后,问题都变得比较简单。只用按照float的IEEE定义与实现取做就行了。
1 | unsigned float_twice(unsigned uf) |
12. float_i2f·
题目:
- 目标: 给定一个int类型的x,返回(float)x的二进制表示。
- 限制:只能使用int或unsigned,其余所有操作都可以使用。
- 最大操作次数: 30
- 难度:4
解法:
本题依然不算难,唯一需要注意的是,int转为float是有精度损失的。因为int是32bit,但是float的小数m只有23bit,在转化时,需要做舍入操作,舍入采用的是就近偶数( nearest even)原则。
我的解法超过了max ops了,并不是最优解
代码:
1 | unsigned float_i2f(int x) |
13. float_f2i·
题目:
- 目标: 给定一个float类型的x,返回(int)x的二进制表示。Anything out of range (including NaN and infinity) should return 0x80000000u.
- 限制:只能使用int或unsigned,其余所有操作都可以使用。
- 最大操作次数: 30
- 难度:4
解法:
同样,按照定义来即可。只是要注意tmin是特殊的,需要单独考虑。另外需要考虑如何判定out of range。
1 | int float_f2i(unsigned uf) |