目录
实验内容
每个函数对操作符的种类和数量做出了限制,一般只能使用位运算符,且自定义的数字不能超过0xff,利用有限的运算完成每个函数的要求。实验提供了了测试函数,btest用于检测结果是否正确,dlc用于检测运算符使用是否满足要求(dlc检测还要求所有的变量声明都位于函数体前面),./driver.pl可全部检查。
注:DataLab和BombLab写的是旧版。
bitAnd
不用&运算符实现与操作。
/*
* bitAnd - x&y using only s~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
int result = ~(~x | ~y);
return result;
}
getByte
一个32位整数有四个字节,根据n = 0,1,2,3,返回x中的第n个字节的数字。
将x中的第n个字节移动到最低位,再跟0x000000ff与即可,高位会被置零,低位保留。
/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) {
int length = n << 3;
int result = 0xff & (x >> length);
return result;
}
logicalShift
c语言中的左移都是逻辑右移,而对有符号数的右移是算术左移,对无符号数的右移是逻辑左移。这里要求实现有符号数的逻辑左移。
正数的算术左移 = 逻辑左移,负数的算术左移的高位被置为1,逻辑左移的高位被置为0。这里的想法是算术左移后再把对应的高位置为0即可,即构造一个数字,二进制下是00...0011...11,前有n个0,后有32 - n个1,跟算术左移的结果与即可。
/*
* logicalShift - shift x to the right by n, using a logical shift
* Can assume that 0 <= n <= 31
* Examples: logicalShift(0x87654321,4) = 0x08765432
* Legal ops: ~ & ^ | + << >>
* Max ops: 20
* Rating: 3
* >>是算术右移
* 负数的算术右移高位是1,需要一个00...0011...11把高位的1去除
*/
int logicalShift(int x, int n) {
//需要00...0011...11 & 算术右移,共n个0,剩下32 - n个1
int arithmetic_shift = x >> n;
//31 - n
int length = (31 ^ n);
//0xffffffff
int base = ~0;
//本应该移位32 - n,这里分成了(31 - n) + 1,移位32位是未定义的所以要拆开!
int temp = ~((base << length) << 1);
int result = arithmetic_shift & temp;
return result;
}
bitCount
要求统计x的二进制中有多少个1,操作符最多40个,个人感觉是最难的一道了,想了很久。
最直接的方法是依次右移,取最低位的值,加到结果中,但是这样操作符会超出很多。
考虑用分治的方法,实际上bitCount的结果 = 把32位中的每一位(0或1)都单独拎出来,再加到一起。考虑用分治的方法进行求和:
- 每1位为一组,共32组,两两相加(第0组与第1组相加,第2组与第3组相加.......)
- 每2位为一组,共16组,两两相加
- ......
- ......
- 每16位为一组,共2组,两两相加
首先说明如何实现两两相加,以第1步为例,即把奇数位与偶数位相加,记一个数字temp1的二进制形式为0101......0101,temp1 & x就是x所有的偶数位,temp1 & (x >> 1)就只剩下了x所有的奇数位,同时与前面的偶数位进行了对齐。那么两者相加就实现了所有奇数位与偶数位的两两相加。注意两个1bit相加的结果长度为2bit,可能是00 01 10,而一共有16对奇偶相加,正好形成了新的16组,所以根据第1步的结果进行第2步计算是正确的。
容易得出每一步所需要的temp分别是
- temp1 = 0x55555555
- temp2 = 0x33333333
- temp4 = 0x0f0f0f0f
- temp8 = 0x00ff00ff
- temp16 = 0x0000ffff
由于只能自定义最大0x000000ff的数字,所以得到上面的每个temp都需要进行移位相加操作,最后发现如果计算每个temp都进行移位相加会超过40个操作符的限制,所以需要更简单的计算temp 的方法。
在尝试了多次后发现,从temp1一步步推出temp16是不行,而从temp16一步步推出temp1是可行的,具体方法为:
- temp16 = 0xff + (0xff << 8)
- temp8 = temp16 ^ (temp16 << 8)
- temp4 = temp8 ^ (temp8 << 4)
- temp2 = temp4 ^ (temp4 << 2)
- temp1 = temp2 ^ (temp2 << 1)
详见代码:
/*
* bitCount - returns count of number of 1's in word
* Examples: bitCount(5) = 2, bitCount(7) = 3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 40
* Rating: 4
*
* temp1 = 0x55555555, binary:01010101...
* temp2 = 0x33333333, binary:00110011...
* temp4 = 0x0f0f0f0f, binary:0000111100001111...
* temp8 = 0x00ff00ff, binary:00000000111111110000000011111111
* temp16 = 0x0000ffff,binary:00000000000000001111111111111111
*
* 首先把每一位当作一个组
* n = (n & temp1) + ((n >> 1) & temp1)
* 新得到的n每两位分一组,每个分组有三种可能(00 01 10)
*
* 接下来用temp2把每一组(2位)两两相加
* n = (n & temp2) + ((n >> 2) & temp2)
* 重复进行即可
*/
int bitCount(int x) {
//快速计算所有的temp
int temp16 = 0xff + (0xff << 8); //0x0000ffff
int temp8 = temp16 ^ (temp16 << 8); //0x00ff00ff
int temp4 = temp8 ^ (temp8 << 4); //0x0f0f0f0f
int temp2 = temp4 ^ (temp4 << 2); //0x33333333
int temp1 = temp2 ^ (temp2 << 1); //0x55555555
int result = x;
result = (result & temp1) + ((result >> 1) & temp1);
result = (result & temp2) + ((result >> 2) & temp2);
result = (result & temp4) + ((result >> 4) & temp4);
result = (result & temp8) + ((result >> 8) & temp8);
result = (result & temp16) + ((result >> 16) & temp16);
return result;
}
bang
实现 !x 操作而不用 ! 运算符。
同样是手动递归 + 分治的方法:
- 高16位 按位或 低16位,得到一个新的16位结果,如果结果中有1说明原32位中有1。
- 对新的16位结果,高8位 按位或 低8位......
/*
* bang - Compute !x without using !
* Examples: bang(3) = 0, bang(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
* 前16位与后16位或得到r
* bang(x) = bang(r)
* 将递归展开写即可
*/
int bang(int x) {
int result;
x = x | x >> 16;
x = x | x >> 8;
x = x | x >> 4;
x = x | x >> 2;
x = x | x >> 1;
result = (x & 1) ^ 1;
return result;
}
tmin
返回32位有符号数最小值,即0x80000000。
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
* 0x80000000
*/
int tmin(void) {
int result = 1 << 31;
return result;
}
fitsBits
给一个有符号数x,问是否可以用n bit表示出来。
n bit其中有一位是符号位,剩下的数字有n - 1位。对于正数,右移n - 1位,x全为0即fit。由于负数是补码存储,所以对于负数,右移n - 1位,x全为1即fit。
/*
* fitsBits - return 1 if x can be represented as an
* n-bit, two's complement integer.
* 1 <= n <= 32
* Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*
* 对于正数,右移n - 1位,x全为0,即fit
* 对于负数,右移n - 1位,x全为1,即fit
* bug见test.c
*/
int fitsBits(int x, int n) {
int result;
n = n + (~0); //n - 1
x = x >> n;
result = !(x) | !(~x);
return result;
}
这里测试时出现了一个小bug,测试程序判断:0x80000000不能用32位表示,显然这个判断是错误的。这里也试了别人写的fitsBits,也是无法通过。故找到test.c中的代码,发现需要稍微修改一下test_fitsBits才行,添加了一个unsigned的类型转换。
int test_fitsBits(int x, int n)
{
int TMin_n = -(1 << (n-1));
//x = 0x80000000, n = 32时会报错
//添加了一个(unsigned)即可顺利通过
int TMax_n = (unsigned)(1 << (n-1)) - 1;
return x >= TMin_n && x <= TMax_n;
}
divpwr2
返回 x / (2 ^ n) 。
正数右移n位即可,有余数自动舍去。负数稍微有点不同,当有余数时需要加1,即判断是否有余数(低n位是否全为0)。
/*
* divpwr2 - Compute x/(2^n), for 0 <= n <= 30
* Round toward zero
* Examples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
* 一个正数右移n位即可,因为不足2 ^ n的部分自动舍1
* 一个负数右移n位,如果其低n位都是0,结果正确
* 若不全为0,由于负数由补码表示,此时应该进1(此时不再是舍1)
* 故当且仅当负数且低n位不全为0,结果需要加1
*/
int divpwr2(int x, int n) {
int is_negative = (x >> 31) & 1;
//若x == (x >> n) << n,则低n位全为0
int is_not_all_zero = !!(x ^ ((x >> n) << n));
int result = (x >> n) + (is_negative & is_not_all_zero);
return result;
}
negate
返回 -x 。
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
* 之前学负数的补码的时候总是记按位取反加1
* 实际上负数可以看成最高位权值为-2^n
*/
int negate(int x) {
int result = 1 + (~x);
return result;
}
isPositive
判断是否为正数,观察符号位即可,再判断是否为0。
/*
* isPositive - return 1 if x > 0, return 0 otherwise
* Example: isPositive(-1) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 8
* Rating: 3
* 右移31位后为0可以判断x >= 0
* 再判断x是否为0即可
*/
int isPositive(int x) {
//前者判断x是否为0,后者判断x是否大于等于0
int result = (!!x) & (!(x >> 31));
return result;
}
isLessOrEqual
判断是否 x <= y 。
讨论何时需要返回1,即 x <= y:
- 异号:y正x负
- 同号:y - x >= 0
正数右移31位 = 0x00000000,负数右移31位 = 0xffffffff,将x和y右移后再异或即可判断是否同号。而y - x可以将x变成对应的负数再与y相加即可,判断是否 >= 0与上一题类似。
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
* x <= y在下述情况下发生
* 异号:y正x负
* 同号:y - x >= 0即可
* 注:判断符号用右移32位判断,0也被算作了正号
*/
int isLessOrEqual(int x, int y) {
//计算同号
int y_minus_x = y + (~x + 1);
//同号 & (y - x >= 0)
int same_sign = !((x >> 31) ^ (y >> 31)) & (!(y_minus_x >> 31));
//(x负号) & (y正号)
int different_sign = (x >> 31) & !(y >> 31);
int result = same_sign | different_sign;
return result;
}
ilog2
返回log2(x),x > 0。
找到最高位的1在哪即可。如果判断从(低到高的)第 i 位是不是最高位的1,只需要把它右移 i 位,看剩下的数字是否为0即可。
不能使用任何循环语句,所以如果采取顺序查找,那么对于32位都要进行上述的判断,运算符数会超。故可以考虑二分查找,查找的 mid 即为最高位1所在的位置,稍微麻烦一点的就是对low和high的更新。
这里令temp = x >> mid,判断temp是否为0就知道它左侧还有没有1了,如果有则向左查找,如果无则向右侧查找。
最初区间为high = 32,low = 0,如果向左(高位)查找,那么就要low += 16,high不变;如果向右(低位)查找,那么就要low不变,high -= 16。
- high = high + (zero & (~16 + 1))
- low = low + (not_zero & 16)
这里当temp == 0时,zero = 0xffffffff,否则zero = 0;当temp != 0时,not_zero = 0xffffffff,否则not_zero = 0。zero和not_zero可以这样定义:
- zero = ((!temp) << 31) >> 31
- not_zero = ~zero
接下来手动将迭代的二分查找展开即可:
/*
* ilog2 - return floor(log base 2 of x), where x > 0
* Example: ilog2(16) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
* 如果有if语句,找到二进制中1出现的最高的位置即可
* 暴力法:
* 一位一位位移,只要当前剩余数字不为0,就对result+1
* 假设1出现的最高位位k,正好加了k次
* 这样估计ops需要每一位需要 ! ! >> + 四个op
* 其中!!x判断是否为0
* 共需要4 * 32 - 1 = 127ops
* 考虑二分:
* 原结果范围为[0, 32)
* 取区间中点16,看x >> 16是否为0
* 若为0,结果范围为(0,16);否则结果范围为[16, 31)
*/
int ilog2(int x) {
//区间长度选为32,保证二分走向哪都可以恰好在第六步后结束
//[low, high)
int low = 0;
int high = 32;
int mid = (low + high) >> 1;
int temp = (x >> mid);
int not_zero, zero;
int result;
//当temp == 0时,zero = 0xffffffff,否则zero = 0
zero = ((!temp) << 31) >> 31;
//当temp != 0时,not_zero = 0xffffffff,否则not_zero = 0
not_zero = ~zero;
//对low的新赋值,当且仅当temp != 0,low = mid
low = low + (not_zero & 16);
//对high的新赋值,当且仅当temp == 0, high = mid
high = high + (zero & (~16 + 1));
mid = (low + high) >> 1;
temp = (x >> mid);
zero = ((!temp) << 31) >> 31;
not_zero = ~zero;
low = low + (not_zero & 8);
high = high + (zero & (~8 + 1));
mid = (low + high) >> 1;
temp = (x >> mid);
zero = ((!temp) << 31) >> 31;
not_zero = ~zero;
low = low + (not_zero & 4);
high = high + (zero & (~4 + 1));
mid = (low + high) >> 1;
temp = (x >> mid);
zero = ((!temp) << 31) >> 31;
not_zero = ~zero;
low = low + (not_zero & 2);
high = high + (zero & (~2 + 1));
mid = (low + high) >> 1;
temp = (x >> mid);
zero = ((!temp) << 31) >> 31;
not_zero = ~zero;
low = low + (not_zero & 1);
high = high + (zero & 1);
mid = (low + high) >> 1;
temp = (x >> mid);
result = low;
return low;
}
需要注意原查找区间长度必须要是32,这样才能保证接下来的每次的查找区间分别达到16,8,4,2,1。
下面是关于单精度浮点数的操作,单精度浮点数包括:S(1符号位)E(8阶码)M(23尾数)尾数包含了隐藏的1。同时有一些特殊的数:
- 阶码全为0时:表示非规格数,例如0x00000000和0x80000000都表示0
- 阶码为全1时:尾数为0表示无穷,不为0表示NAN
float_neg
以unsigned传入uf,但它被解读为一个float形式的数字,要求返回相反数。如果uf是NAN,返回uf本身。
/*
* float: 1(符号S) 8(阶码E) 23(尾数M)
* E = e + 127
* 对于5的默认舍入方式为向偶数舍入
* 阶码全为0时:表示非规格数,例如0x00000000和0x80000000都表示0
* 阶码为全1时:尾数为0表示无穷,不为0表示NAN
*/
/*
* float_neg - Return bit-level equivalent of expression -f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representations of
* single-precision floating point values.
* When argument is NaN, return argument.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 10
* Rating: 2
* 判断是否为NAN,不是则uf和0x80000000异或即可
* uf ^ 0x80000000,uf的最位取反,其他位都保持不变
*/
unsigned float_neg(unsigned uf) {
unsigned E = uf & 0x7f800000;
unsigned M = uf & 0x007fffff;
unsigned change = 1 << 31;
if (E == 0x7f800000 && M != 0) {
return uf;
} else {
return uf ^ change;
}
}
float_i2f
将int型的x转换成float类型,用unsigned类型的result返回。这里可以使用循环、定义任何数字。
思路就是分别计算符号S,阶码E和尾数M。
- 符号位S很好计算
- 阶码E计算时,如果x是负数,则要把x取反变为对应的正数,再利用循环移位。
- 尾数M只能有23位,稍微麻烦点。计算阶码E的时候可以知道x的实际阶数e(有多少有效数字),如果e <= 23很简单,如果超过23就需要计算是否有舍入。
- 舍入规则是:舍去部分超过0.5则进1,如果恰好等于0.5且原数字本来是奇数,则也要进1,其余所有情况都正常舍去。
关于计算进位的部分详见代码注释:
/*
* float_i2f - Return bit-level equivalent of expression (float) x
* Result is returned as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point values.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
* 分别计算S,E,M是多少,将三者相加即为结果
* 麻烦的是需要舍去低位时:
* 当要舍去一部分时,要考虑的有
* 保留部分最后一位(last),舍去部分最高位(first),舍去部分去掉最高位的剩余部分(remain)
* 逻辑是first == 1 && remain != 0 进位
* first == 1 && remain == 0 && last == 1进位
* 这里判断某一位是否为1,将该位移动到最高位,判断数值是否小于0
*/
unsigned float_i2f(int x) {
unsigned S = 0; //默认为正数
unsigned e = 1; //阶数
unsigned E = 127; //阶码中偏移值存入E
unsigned M; //尾数
unsigned result; //结果
int last, first, remain; //计算进位需要
if (x == 0) {
return 0;
} else if (x == 0x80000000) {
return 0xcf000000; // 无法直接-x
} else if (x < 0) {
S = 1;
x = -x; //x = |x|
}
while (x >> e) {
++e;
}
--e; //减去隐藏的1
E = E + e;
if (e < 23) {
x = x << (23 - e);
} else if (e > 23) {
last = x << (54 - e); // 54 - e = (32 - e) + (23 - 1),将保留部分的最低位左移到最高位
first = last << 1;
remain = first << 1;
x = x >> (e - 23);
if (first < 0) {
if (remain != 0 || last < 0) {
++x; //有进位
//进位后x低23位全为0,说明第24位(隐藏位)有进位,第25位 == 1
if (x >> 24) {
++E;
}
}
}
}
M = x & 0x007fffff;
result = (S << 31) + (E << 23) + M;
return result;
}
float_twice
计算所给单精度浮点数的两倍并返回。
正常的数字直接阶码 + 1即可,需要考虑非规格化数字和NAN。
非规格化的数字阶码 == 0,且M没有隐藏的高位1,所以直接M左移1位即可,如果M最高位就是1,左移将移动到E上,恰好转换成了规格化数,不需要额外判断。
/*
* float_twice - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
* 阶码+1即可,对非规格浮点数和特殊值讨论一下即可
*/
unsigned float_twice(unsigned uf) {
unsigned S = uf & 0x80000000;
int E = uf & 0x7f800000;
int M = uf & 0x007fffff;
//未规格数M左移一位
if (E == 0) {
//若M最高位为1,左移后正好移到了E上,E置为1正好
M = M << 1;
return S + E + M;
}
//结果是NAN
if (E == 0x7f800000) {
return uf;
}
return uf + 0x00800000;
}