CSAPP Datalab
Datalab 笔记
旧版本
bitAnd(x,y)
1 | /* |
可以参考德摩根定律
getByte
1 | /* |
第二题是从 x 中提取出第 i 个字节(i=0,1,2,3),方法就是将那个字节移位至最低位,然后用屏蔽码 0xff
提取就可以了
3.logicalShift
1 | /* |
第三题要求实现逻辑右移,对于有符号的 int
,C 语言默认的移位方式是算术右移,就是右移时在高位扩展符号位,这里我们需要扩展的符号位都设置为 0 ,可以构造一个屏蔽码屏蔽 x>>n
中的非扩展的位,用 & 实现目的。
但这里要注意 C 语言对移位位数超出自身长度的行为是未定义的,因此在这里构造屏蔽码时不能使得移位位数超过了32或是小于0,我这段代码为了避免这种情况的发生,将屏蔽码分了最高位和其他位两部分构造,直接使用 ((0x1<<(33+~n))+~0)
构造的屏蔽码在 n=0 将会无法确定。
这里 32+~n
表示了 31-n ,可以由补码的运算性质 -x=~x+1
得到,同时这里我在注释里写了两个我最初写的 bug 。
4.bitCount
1 | /* |
这题好难T_T,应该是这个 lab 里最难的了吧;
题目意思就是要统计一个32位的 int
里的 1
的个数,但是只能使用40个操作符,直接扫一遍字的话操作符就大大超过规定数了;
这里构造了五个常数,分别是 0x55555555,0x33333333,0x0f0f0f0f,0x00ff00ff,0x0000ffff
,就是分别间隔了1个0,2个0,4个0,8个0和16个0,利用这五个常数就能依次计算出五个值,第一个值每两位的值为 x 的对应的两个位的和(即这两位中 1
的数目),第二个值每四位是第一个值对应的四位中两个两位的和(即原 x 中 1
的数目),依次类推最后一个值就是结果了;
怎么理解呢,可以看到这里构造的五个常数的间隔可以刚好使得只提取 n 位,移位之后再提取相邻 n 位(n=1,2,4,8,16),并且(考虑最大值可知)这两个 n 位加和后不会超出 n 位,使得 x 中的 1
一步步加和成最终的结果,可以举一个例子,若要求 1001 中 1
的数目,用(1001&0101)+((1001>>1)&0101)
,就能将每相邻一位加和成一个两位,成 0101,再用(0101&0011)+((0101>>2)&0011)
,就将每两位加和了,得到 0010 ,就是最终的结果。
5.bang
1 | /* |
这题要求仅用规定的操作符来实现!运算,对 0 运算就得到 1,对非 0 就得到 0;也就是如果 x 的位中含有 1 就返回 0 ,这里运用移位后取或将 x 中的位一步步「折叠」 到了第一位上,然后判断第一位就可以了,这种「折叠」的方法很有趣,值得一看:)
2.补码
6.tmin
1 | /* |
这题返回补码最小值,注意到 tmin==~tmax
,补码负数表示部分和正数是不对称的,最小值的绝对值是最大值的绝对值加1。
7.fitsBits
1 | /* |
这题题目意思是判断 x 能否用一个 n 位的补码表示,能的话就返回 1,开始我没看懂题目…举个例子,101 是不能用 3 位补码表示的,因为 3 位最高位是符号位,最大只能表示 011,注意到这里 x 是32位的,不能直接右移的;
要用 n 位的补码表示,x 只能是两种情况: 00…0|0|(n-1)位
或是 11…1|1|(n-1)位
,这样 32 位的补码才会与 n 位的补码值相同,这里的方法就是将 x 左移(32-n)再右移回去,这样就能得到那两种情况的值,再判断这样操作之后是否与原来的 x 相等,就解决问题了;
这里由补码性质,33+~n
等于 32-n
。
8.divpwr2
1 | /* |
这题计算 x/(2^n) ,注意不能直接右移,直接右移是向下舍入的,题目要求是向零舍入,也就是正数向下舍入,负数向上舍入,这里参照 CS:APP 书上的做法,给负数加上一个偏正的因子 (0x1<<n)+~0)
,判断负数直接看符号位。
9.negate
1 | /* |
这题求 -x ,直接利用补码的性质 -x=~x+1
就可以了。
10.isPositive
1 | /* |
这里判断是否是正数,直接判断符号位,但是注意要排除 0 的情况!
11.isLessOrEqual
1 | /* |
这题比较两个数的大小,要求判断第一个数是否小于等于第二个数,这里考虑做减法然后判断符号,注意要考虑溢出的情况,这里 ((x+~y))
表示了 x-y-1
,若其结果为负,则 x <= y ;
这里先判断 x 与 y 的符号,如果 x 为负,y 为正直接返回 1 ,如果 x 为正,y 为正,直接返回 0;然后就是全正数和全负数的减法,这样不会溢出。
12.ilog2
1 | /* |
这题求 x 以 2 为底的对数,解法有点难想到,注意到 32 位数的对数最大也不会超过 32,可以写成是 16*a+8*b+4*c+2*d+e
这里 a,b,c,d,e 都是 0 或 1,然后通过向右移 16 位就可以判断符号就可以得到 a ,右移 16*a+8 位可得到 b,以此类推得到其他位。
3.浮点数
以下三题是关于浮点数的,可以使用任何操作符和分支语句。
13.float_neg
1 | /* |
这题计算 -f ,f 是浮点数,这里直接改浮点数的符号位,但是注意要单独考虑 NaN 的结果。
14.float_i2f
1 | /* |
这题是将整型转化为浮点数的格式,坑点很多,耗时长。。
整体思路就是依次计算符号位,阶码值和小数字段,符号位可以直接移位提取,阶码值就是除了符号位外最高位的位数减 1 再加上偏差 127,小数字段可以移位(负数可以化为正数操作)获得,但这问题没这么简单,有很多坑点:
1.特殊值 0 化为浮点数后是非规格化的,单独考虑;
2.特殊值 0x80000000 是 2 的整数倍,小数部分用移位的话因为舍入问题会溢出,单独考虑;
3.要仔细考虑移位过程,左移还是右移能得到 23 位的小数部分;
4.注意舍入问题,这里需要仔仔细细地考虑清楚,默认使用向偶数舍入,就是舍去部分小于中间值舍弃,大于中间值进位,为中间值如 100 就向偶数舍入:就是看前一位,进位或舍弃总使得前一位为 0;
5.最后就是操作数目限制在 30 位,我最开始写完的代码有 42 个操作符,应该是算法太麻烦了。。写完最后要一步步简化操作符数目,控制中 30 以内,这里我为了减少操作符数目,写了些可读性很不高的表达式,还用了不少变量如 cp,cp2,简化这些耗了我很多时间。
15.float_twice
1 | /* |
这题计算浮点数的两倍,无穷大和 NaN 时直接返回,然后分规格化和非规格化两种讨论:
规格化的情况,阶码值直接加 1 ,但是有个特殊情况就是加一后阶码为 255 时,应返回无穷大;
非规格化的情况,排除符号位左移一位就可以了,因为这时阶码值为 0 ,两倍就相当于小数字段左移一位,不用担心溢出的情况,溢出时阶码值加 1,小数字段左移一位,相当于整体左移了。
新版本
bitXor(x,y) 只使用 ~ 和 & 实现 ^
1 | /* |
tmin() 返回最小补码
有符号数是用补码来表示的,Tmin表示最小补码数,对于1个字节大小的补码,最小补码数形式为1000 0000(最小的有符号数,符号位为1,其余都是0),C语言中int类型占4字节,即32位,所以对1左移31位来构造最小补码。
1 | /* |
isTmax(x) 判断是否是补码最大值
函数功能是判断x是否是有符号数的最大值,也就是补码最大值,还是拿1个字节来看,最大补码数的形式为0111 1111,代码中的neg1是为了将-1单独判断出来,因为如果只使用return后面那句(!(~(x+1)^x))的话,会导致当x=-1的时候也会返回1,判断出现错误,而改变后的返回结果可以排除-1的干扰。
1 | /* |
allOddBits(x) 判断补码所有奇数位是否都是1
构造掩码操作即可,将掩码和x进行与操作,可以让x的奇数位置不变,而偶数位置变为0。
1 | /* |
negate(x) 不使用负号 - 实现 -x
1 | /* |
isAsciiDigit(x) 判断 x 是否是 ASCII 码
通过上下界来判断输入的x是否在0x30~0x39的范围中,使用x分别加上界和下界,当x不在这个范围中时,通过判断符号位的变化来得出判断。
1 | /* |
conditional(x, y, z) 类似于 C 语言中的 x?y:z
重点在于return语句,这个操作可以根据x的不同来返回不同的值
1 | /* |
isLessOrEqual(x,y) x<=y
判断方法:如果x和y同符号,当x<=y则返回1;或者如果x和y不同符号,那么当x<0则返回1;其余情况返回0。
这里根据y-x的结果的符号来判断x和y的大小。
1 | /* |
logicalNeg(x) 计算 !x (不用 ! 运算符)
1 | /* |
howManyBits(x) 计算表达 x 所需的最少位数
1 | /* howManyBits - return the minimum number of bits required to represent x in |
floatScale2(uf) 计算 2.0*uf
需要了解计算机内浮点数的表示方法,了解浮点数中的规格数、非规格数、无穷大和未定义的区别和表示。
我们先看如何表示浮点数:
这里的uf类型为unsigned int,并不是浮点数,但是我们将uf看作为单精度类型,它有32位,最高位是符号位,之后8位保存指数信息,最后23位保存小数信息,所以在代码中我们可以看到,我们通过和0x7F800000取与操作来获得指数信息,再右移23位取出这一部分。
浮点数有几种特殊情况:
1.若exp部分全为0(exp = 0),则是非规格化数,它是一种非常接近0的数;
2.若exp部分全为1(exp = 255),当小数部分全为0时,表示无穷大;当小数部分不为全0时,表示未初始化数据NaN;
3.以上两种情况以外,就是规格化数。
所以我们需要判断uf是哪一种浮点数,并根据它的类型来进行相应的操作。
1 | /* |
floatFloat2Int(uf) 计算 (int) f
需要了解整数和浮点数之间的转化方法,我们要做的就是将浮点数中的指数部分和小数部分取出来,然后通过这两部分来转化为整数,具体操作可以看代码,在这个过程中还要判断是否会产生溢出,以及浮点数是否为规格数等情况,如果产生溢出,我们需要返回一个特定的溢出值。
这里有一个将整数转化为浮点数的例子:
floatPower2(x) 计算 2.0的x次方
1 | /* |