CSAPP_DATALAB REPORT
一、实验目的
本实验目的是加强学生对位级运算的理解及熟练使用的能力。
二、报告要求
本报告要求学生把实验中实现的所有函数逐一进行分析说明,写出实现的依据,也就是推理过程,可以是一个简单的数学证明,也可以是代码分析,根据实现中你的想法不同而异。
三、函数分析
- bitAnd函数
函数要求:
函数名 | bit And |
---|---|
参数 | int x,y |
功能实现 | 实现与运算 |
要求 | 只能用非运算和或运算,且操作数在8次内 |
分析:
x&y本质上就是保留x和y都为1的二进制位,所以就可以先找到x或y为0的二进制位,再取反即可
函数实现:
int bitAnd (int x, int y) {
return ~((~x)|(~y));
}
- getByte函数
函数要求:
函数名 | getByte |
---|---|
参数 | int x,n |
功能实现 | 求x的第n个字节 |
要求 | 只能用!~ & ^ | + << >>在6次操作之内完成 |
分析:
一个字节就是8个二进制位,对应两个十六进制位,所以对x只需要右移8n位,在取十六进制下的最低两位也就是二进制下的最低8位即可
函数实现:
int getByte(int x,int n){
int a=0xFF;
n<<=3;x>>=n;
return x&a;
}
- logicalShift函数
函数要求:
函数名 | logicalShift |
---|---|
参数 | int x,n |
功能实现 | 求将x逻辑右移n位后的值 |
要求 | 只能用!~ & ^ | + << >>在20次操作之内完成 |
分析:
逻辑右移,区别于算术右移,即不论是否为负值,右移后最高位始终补0,。普通的x>>n对于正数x将返回正确的数值,而对于负数会出现符号位右移而使高位全部为1的情况。所以要做的只是消除多余的1,也就是把负数右移n位后的高n位全部归0,同时保持低32-n位即可。
函数实现:
int logicalShift(int x,int n){
int a=1<<31;
a>>=n;a<<1;a=~a;
x>>=n;
return x&a;
}
- bitCount函数
函数要求:
函数名 | bitCount |
---|---|
参数 | int x |
功能实现 | 求x的二进制表示中1的个数 |
要求 | 只能用!~ & ^ | + << >>在40次操作之内完成 |
分析:
首先可以想到暴力判断每一位是否为1,但每次判断及更新答案我只能做到最优3次操作,这样的总操作数不小于3x32=96,超40两倍多。但对于1的数量,又不得不判断每一位,所以要做的就是加速枚举。因为32=4*8,所以我们可以做到每次枚举4位,分8组完成枚举,即第一次枚举0,8,16,24四位,第二次是1,9,17,25,以此类推。然后去掉高位的权值加到低位,低位就是答案啦。
函数实现:
int bitCount(int x){
int a=(1<<8)+1,ans,p;
a+=a<<16;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;x>>=1;
ans+=x&a;
ans=ans+(ans>>16);
ans=ans+(ans>>8);
p=63;
return ans&p;
}
- bang函数
函数要求:
函数名 | bang |
---|---|
参数 | int x |
功能实现 | 输出!x |
要求 | 只能用~ & ^ | + << >>在12次操作之内完成 |
分析:
输出!x,相当于判断x是否为0。所以我分负数和正数两种情况进行操作。case 1:对于负数可以取其符号位表示其非0;case 2:对于正数,我们可以发现其取值在1~231-1之间,所以我们可以将其加上-231使其拥有符号,在加上-1使0在这种情况下发生负溢出失去符号,再像负数一样判断符号位表示非0即可。
函数实现:
int bang(int x){
int a,b,c,d;
a=(1<<31);
b=a&x;b>>=31;b&=1;
c=0;c~=c;
x=x+a+c;
d=a&x;d>>=31;d&=1;
d|=b;b^=1;
return b;
}
- tmin函数
函数要求:
函数名 | tmin |
---|---|
参数 | 无 |
功能实现 | 二进制补码表示下最小的数(其实就是求最小int类型的数) |
要求 | 只能用!~ & ^ | + << >>在4次操作之内完成 |
分析:
int类型范围是-231~231-1,故输出-231
函数实现:
int tmin(void){
return (1<<31);
}
- fitsBits函数
函数要求:
函数名 | fitsBits |
---|---|
参数 | int x,int n |
功能实现 | 询问x能不能用n位二进制补码表示 |
要求 | 只能用!~ & ^ | + << >>在15次操作之内完成 |
分析:
n位二进制补码表示x,即x的高32-n位不能存在有意义的位,当然,符号位例外。换句话说,对于正数x,x的二进制高32-n位不含1;对于负数x,高32-n位不含0。所以问题转化为比较,而且需要将正负分类比较统一起来(因为不能用if),又因为n位表示的正数第n位必须是0,左移32-n位再右移32-n位后可以使高位全0(第n位为1的正数这样操作后会因符号位变1而直接排除);同时,n位表示的负数,符合条件的x第n位一定为1,同样的操作可以保证高n-32位为1(若第n位为0,操作后符号位变0而排除)。因为移位操作下低n位不变,所以只需要亦或x和移位后得到的数值即可判断可否表示。
函数实现:
int fitsBits(int x,int n){
int a,b;
a=32+(~n+1);//~n+1实现n取相反数,在后面的函数中有单独实现的详解
b=x<<a;b>>=a;x^=b;//这个部分感觉还是有点绕,不过一共2*2=4种情况分类讨论即可
return !x;
}
- divpwr2函数
函数要求:
函数名 | divpwr2 |
---|---|
参数 | int x,int n |
功能实现 | 求x/2n |
要求 | 只能用!~ & ^ | + << >>在15次操作之内完成 |
分析:
还是因为二进制补码带来的问题,虽然正数可以直接右移表示除2n,但负数直接右移会导致误差。不过正数可以直接右移给了负数部分的启发:负数可以先变正数进行操作,之后变回负数。所以这又变成了一个统一正负分类的问题了。得益于操作数上限是15,足够进行额外的操作进行讨论。即利用-1的补码全1,0的补码全0,使其‘与’相应的答案使其有效或无效。
函数实现:
int divpwr2(int x,int n){
int a,b,c;
a=0;a~=a;c=a;//小技巧,0取反是-1,避开了使用禁止的‘-’
a+=x;a>>=31;//取符号位,利用算术右移补符号位的优势
b=a^c;//a对应负数有效,b对应正数有效
b&=x>>n;
x=~x+1;x>>=n;x=~x+1;a&=x;
return a+b;
}
- negate函数
函数要求:
函数名 | negate |
---|---|
参数 | int x |
功能实现 | 求-x |
要求 | 只能用!~ & ^ | + << >>在5次操作之内完成 |
分析:
二进制补码,对正数而言即原码,对负数而言即原码除符号位外取反加1。所以取相反数时首先需要修改符号位,其余低位取反加1即可,恰好对应~x+1。
函数实现:
int negate(int x){
x=(~x)+1;
return x;
}
- isPositive函数
函数要求:
函数名 | isPositive |
---|---|
参数 | int x |
功能实现 | 判断x是否为正 |
要求 | 只能用!~ & ^ | + << >>在8次操作之内完成 |
分析:
判断x是否为正,可以判断x是否小于等于0,这里还是分类讨论一下。对于负数,可以取符号位判断;对于0,可以取逻辑非。两者取或,即满足其中任意一项者即小于等于0,再取非得x是否为正数。
函数实现:
int isPositive(int x){
int a=1<<31;
a&=x;a>>=31;a&=1;
x=!x;a|=x;a=!=a;
return a;
}
- isLessOrEqual函数
函数要求:
函数名 | isLessOrEqual |
---|---|
参数 | int x,y |
功能实现 | 判断x<=y正确与否 |
要求 | 只能用!~ & ^ | + << >>在24次操作之内完成 |
分析:
这题我还是分了两种情况,使用-1/0(有效/无效)来实现的。case 1:x,y异号,这种情况直接可以根据谁有负号谁小来判断;case 2:x,y同号,这样可以使y=y-x(因为同号所以不存在溢出的问题),再判断y有无负号来判断大小。
函数实现:
int isLessOrEqual(int x,int y){
int a,b,c,d;
a=1<<31;b=a;c=a;
b&=x;b=!b;
c&=y;c=!c;
d=b^c;c&=(!b);
y+=~x+1;
a&=y;
a=!a;a&=!d;
d&=c;
return a+d;
}
- ilog2函数
函数要求:
函数名 | ilog2 |
---|---|
参数 | int x |
功能实现 | x对2取对数并下取整 |
要求 | 只能用!~ & ^ | + << >>在90次操作之内完成 |
分析:
首先考虑暴力,枚举每一位有无来进行答案更新,但这样操作数不少于3x32=96次>90次。所以还是加速枚举,考虑到x的最大值为231-1,即答案小于31。于是可以利用倍增的思想31=16+8+4+2+1,即从大到小枚举2的相应次方,从而加速枚举。
函数实现:
int ilog2(int x){
int ans=0,p,o,t;
o=~0;
p=x>>16;p=!p;p+=o;t=p&16;ans+=t;x>>=t;
p=x>>8;p=!p;p+=o;t=p&8;ans+=t;x>>=t;
p=x>>4;p=!p;p+=o;t=p&4;ans+=t;x>>=t;
p=x>>2;p=!p;p+=o;t=p&2;ans+=t;x>>=t;
p=x>>1;p=!p;p+=o;t=p&1;ans+=t;
return ans;
}
四、实验总结
总结分析:
- 关键点:
- 不能用减号,有的时候需要减1,则需要加-1,而-1是0按位取反的结果。
- -1的性质非常特殊,因为它的补码表示为全1,而加1得到的0补码为全0,可以用来实现if的操作
- 不能用if不意味着不能分类讨论,不能用循环控制不意味着不能使用重复操作
- bitCount函数中按位枚举的优化后的ans处理最初是&(28-1)导致高位的大权值1被计算在内,后来发现结果不超过32,选择了63作为与的参数(因为63=32+16+8+4+2+1)。
- bang函数的难点就在于对于正数的处理,因为正数没有统一的与0不同的位置,所以要考虑使其转化为负数,同时0不被转变为负数,考虑相对位置的不变性,使0在-231之后减一溢出是关键。
- fitsBits函数分四类讨论看似很复杂,但有两种情况会被直接排除,导致程序实现并不复杂。
- divpwr2函数还是要分类讨论,正数的处理通过负数取反来延伸很重要。
- isLessOrEqual函数要注意不能直接y-x,这样对大正数减大负数或大负数减大正数的情况会导致溢出,从而得不到正确答案。
- ilog2函数暴力枚举倍增优化是acm及oi的常见知识点,可以将O(N)算法复杂度降至O(lgn)级别,与本题的应用场景十分契合。
实验建议
- 好像没有给关于浮点数类型的实验,感觉最好还是布置一下。
- 感觉有些分值不高的题目如fitsBits及divpwr2函数的实现难度大于isLessOrEqual等分数较高的题目。但毕竟是个人感觉,可能不准,可以调查一下同学的感受然后更改题目分值及顺序。
- 可能是个人做题习惯,很多函数中出现了~0得到-1的操作,感觉很多题目大同小异,甚至出现一个函数包含另一个函数内容的情况,大概可以精简一下题目,增加一下考察^等使用频率较少的操作的考察。
- 前面相当多的题目还是在考察位运算二进制数,比较表层,可以适当考察一下实际应用中的如二进制状态压缩这类知识的考察,增加如最后一个ilog2一类的算法考察,也许能起到更好的效果。