该篇内容为原博客博文,原上传于2022年4月30日。
话说从寒假开始半摸半读《CS:APP》,但是一直没有写Lab。都说9个Lab是全书的精华,因此深感有必要做一下,所以前阵子抽空先把DataLab做了。虽说是第一个(据说也是最简单的)Lab,却也做的磕磕绊绊,深感自己码力不足。总之最后是完成了,还是写篇博客记录一下,也方便自己日后review。
1分题
1分题基本都是签到题,难度不大。
bitXor
1 | /* |
只使用位与和位非实现位或,运用De Morgan’s laws就行了。
tmin
1 | /* |
返回补码表示的最小数,即0x8000_8000,也即int类型能表示的最小数,也没有什么好说的。
2分题
开始有些技巧性的操作出现了。
isTmax
1 | /* |
要求如果x是补码表示的最大数就返回1,否则返回0。
那么我们首先要能识别出补码最大数,并且能将其转化为0x1作为结果。显然0x7fff_ffff就是补码最大数,如何利用允许使用的运算符将其转化为0x1呢?这里就比较tricky了。我们可以取x + 1 + x,如果x是0x7fff_ffff时,将得到0xffff_ffff,即全1。对全1取位反即可得到0,再对0取逻辑非就能得到0x1了。
但这里有一个容易忽略的情形,即当x为0xffff_ffff时,由于溢出,x + 1 + x也能得到同样的结果(我是在btest时才发现这个特例),所以需要排除这个情形,使得只有当x=0x7fff_ffff时才能返回0x1。为此,我们可以不着急对0取逻辑非,而是先再对原来的x+1取逻辑非,再加给取位反后的x + 1 + x。当x为0x7fff_ffff时,这一步多出的操作不会对结果造成任何影响,因为此时x + 1取非后得到的是0,加个0显然不会有影响;但对于x为0xffff_ffff时,x + 1取非后得到的是1,加回去后得到1,再取逻辑非时就变成了0,从而排除了这个例外。
allOddBits
1 | /* |
从0开始计位,若x所有奇数位都为1则返回1,否则返回0。
如何提取x的奇数位呢?显然用一个奇数位为1,偶数位为0的mask就行了,但我们不能直接用int mask = 0xAAAAAAAA构造出这个掩码,因为Lab要求整数实验的代码最多使用8位常数,如0xAA,所以我们首先要用0xAA产生0xAAAAAAAA,方法有很多,这里就不再赘述。再用这个mask与x位与,即可在结果的奇数位上对应得到x的奇数位。如何判定x的奇数位是否全为1呢?我们很容易想到如果如此,那么x位反后再与mask位与,应当得到全0,因为一个是奇数位全0,偶数位全1,另一个是奇数位全1,偶数位全0,没有一位是相等的。再对结果取逻辑非就能得到1。而对于不满足的x,检验可知经同样的运算,最终都会得到0。
negate
1 | /* |
返回x的相反数。只要对补码表示有基本了解,这题应该都是秒过,不再赘述。
3分题
难度更大了。。。
isAsciiDigit
1 | /* |
若x是0~9数字的ASCII码表示,则返回1,否则返回0。
只要我们能比较x同0x30和0x39的大小就行了。如何用位运算比较两个数之间的大小呢?(我不道啊?看了别人的写法才懂)有一种比较tricky的处理方式,虽然这题不给用减法,但我们可以借鉴类似的想法,即自己构造出会使得符号位改变的临界数,再看符号位就行了。具体思路可以看代码注释。
conditional
1 | /* |
这里的难点在于如何从x的位级表示获取其一位布尔值,使用逻辑非即可。第一次逻辑非会把0转化为1,非0转化为0,第二次逻辑非会还原为原逻辑值,所以需要2次逻辑非。但我们不能直接用这个一位布尔值,因为要实现选择操作,用一位只能保留原输入的一位,显然是不对的,故还需经一步得到全1和全0,再做与操作。
isLessOrEqual
1 | /* |
比较x,y大小,返回比较结果。
第一眼看,我们也许会想用isAsciiDigit的思路做这题,但这是不行的。因为上一题我们只考虑0x30~0x39这一正数小区间,而现在我们需要考虑整个实数域。用相同的方法处理y为负数时会出现问题。
这时就要回退到一般运算思路,即符号不同正数为大,符号相同看差值符号即可。获取差值时,这里不能直接用’-‘,故用补码方式。
x,y和y-x的符号不难获得,但难点在于如何通过位运算在符号不同时正确选择正的那一个数,思路可参见代码注释。
4分题
这几位更是重量级。
logicalNeg
1 | /* |
实现逻辑非运算符。要做出这题,首先需要清楚C语言中的右移对于有符号数是算术右移,以及整数中的1000…0000表示最小数而不是表示负零。其次,还是要想办法将位级表示的x的信息用一个比特表示出来,之前我们都是用!!,但这题就是需要我们实现!,故得采取新的思路。想到对于0来说,其原码和补码都是0x0000,符号位相或仍为0;而对于非0数,其源码和补码的符号位必然互补,从而相或时为1,进而就可以区分0和非0数了。最终要求是求反,故再加1即可。
真正实现起来,一行代码即可搞定,但背后的思考过程不是那么容易,你得想到0和非0数各自相反数符号位的差异,才能做出这题。
howManyBits
1 | /* howManyBits - return the minimum number of bits required to represent x in |
求用补码表示x至少需要几位。思路很好想,符号位必定占一位,剩下的从左往右找第一个有效位(正数是1,负数是0),其与之后剩下的位都是需要的位。但是代码实在写不出来,就copy别人的代码了。
实际上代码的思路也很简单,就是在这串32位比特上做二分搜索(还能这样,艹),找到第一个有效位,同时统计比特数量。
这里还有一个小插曲,即使用的临时变量都必须在函数的一开始提前声明,不能在使用时才声明并初始化,否则dlc检查时会报错,说你没有声明变量。不太清楚原因。
floatScale2
1 | /* |
实现2*f。与前面的题相比,这题其实不难,只要认真读了书,知道浮点数的表示方法就能做出来。
若为规格数,直接阶码加1即可。但若阶码加1后变为全1,则返回的应是对应符号的无穷大。
若为非规格数,则直接左移1位并补上符号位即可。这是因为当非规格数的小数字段最高位为0时,直接左移1位仍表示为非规格数,且小数字段因左移而乘以2,自然得到了结果
当小数字段最高位为1时,左移1位会导致阶码变为0x1而成为规格数。但对规格数而言,0x1阶码与非规格数表示的指数相同,都为1-Bias。且虽然小数最高位看似丢失了,但规格数表示的尾数本身就是1+f,与非规格数的尾数f相比,相当于把最高位的1表示成十进制的0.5乘以2得到的1单独提取了出来,其余位左移自然乘以2。得益于IEEE浮点标准的这种非规格数平滑向规格数过渡的设计,我们这里处理非规格数也简单很多,不需要担心溢出。
若为无穷大和NaN,直接返回原数即可
floatFloat2Int
1 | /* |
实现float到int强制类型转换。这题也没写出来,copy了网上代码,代码解释看注释就行。
floatPower2
1 | /* |
实现2的幂次方运算。这题简单一点,首先由x加上bias得到对应浮点数的阶码。由于2的幂符号位为0,尾数也为全0,故再对阶码移位到浮点数对应的位置即可。注意要排除极小数和无穷,即超过阶码表示的数。
结果
这里有个小插曲,编译btest时提示不兼容的gcc。这是因为我的机器是x86-64,但Lab的makefile中添加了-m32参数,而我缺少在64位机器编译32位程序的库。这里可以选择修改makefile,把这个参数删去或者改为-m64,或者安装需要的库,有gcc-multilib和module-assistant。
测试结果如图。
碎碎念
从这个学期开始,我一直处于一种浑浑噩噩的状态,课内没学好,加权卷不上去,课外的知识也进度缓慢(包括但不限于CSAPP只读到了第四章,计算机视觉半熟不熟,几个月没写安卓)。最近看了许多大牛学长的博客和他们的经历分享,收获了很多,还是希望能向强者靠拢。开始做CSAPP的lab,也算是自己挣脱摆烂状态的一次尝试。希望自己能继续努力,保持学习吧!