CSAPP Datalab详解
文章目录
实验环境
OS: Ubuntu18.04LTS 64位
IDE: CLion
环境配置注意事项:
如报错:fatal error: bits/libc-header-start.h: No such file or directory。
可能的原因是因为在64位机器上一般只有64位编译所依赖的库。解决方法为运行:
sudo apt-get install gcc-multilib
Lab要求
文件结构如下:
Makefile - Makes btest, fshow, 和 ishow (生成需要的可执行文件)
README - Lab使用说明
bits.c - 需要修改的文件
bits.h - 头文件
btest.c - 测试文件
btest.h - 测试文件的头文件
decl.c - build 测试文件的依赖文件
tests.c - 测试文件的依赖文件
tests-header.c- 测试文件的依赖文件
dlc* - 可执行的规则检查程序,使用btest之前应先使用dlc对bits.c进行规则检查
driver.pl* - 打分文件,可以不管
Driverhdrs.pm - 打分文件配置
fshow.c - 检查浮点数表示的工具
ishow.c - 检查整数表示的工具
根据Lab要求,只需要修改bits.c文件中内容即可
关于可执行文件的具体使用要求,请见ReadMe文件,请务必仔细阅读如何使用ishow,fshow, btest工具帮助DEBUG。
关于代码规则的具体要求,定义在bits.c中,请找到并仔细阅读,以免不通过dlc的代码规则检查。
实验内容
在实验内容开始前,先简单介绍函数上注释是什么意思,以bitXor上的注释为例:
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
}
Example:给出一个使用的例子。
Legal ops: 在本函数你只能使用这些运算符
Max ops: 使用运算符的数量不能超过所给值(这里为14)
Rating:占分为1分(这个不用管)
下面正式开始实验部分啦
bitXor
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
int left = ~(x & (~y));
int right = ~((~x) & y);
return ~(left & right);
}
如下为计算a,b两数做异或运算的公式
a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
由于题目限制,题目不允许使用| OR运算符,使用德摩根律 进行转化,转化后即可得到符合要求的实现方法。这里不详细展开了。
tmin
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
int byte1 = 0x01;
return byte1<<31;
}
很简单的一个,在32位情况下,最小值为0x80000000
isTmax
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
int isTmax(int x) {
int min = x + 1; //1,000
int all_one = min + x; //1,111
int x_ = ~all_one; //0,000
min = !min; //min = 0,000
x_ = x_ + min;
return !x_;
}
如果x是Tmax(0x7FFFFFFF),则Tmax + 1 溢出,成为 Tmin(0x80000000),若在这种情况下使x 与Tmin相加,即可得到0xFFFFFFFF。之后对这个比特位全1的数取反、取非后即可得到1。
但这并不是正确的想法因为如果x是-1(0xFFFFFFFF)经过上述流程,其结果同样为1。这是不正确的,所以我们应当想办法将-1从结果中筛出去。如何剔除-1呢?
我们需要关注二者不同的地方,他们唯一的不同在于:执行x+1后值不同
Tmax + 1 = Tmin
-1 + 1 = 0
在代码中我们用min变量记录+1后的结果。取非后与x_变量相加,即可提出-1带来的错误情况。
PS:如果你觉得使用32位进行描述感到头晕,在解题时可以使用3位,或4位的二进制系统进行模拟,这样举例子你很快就能发现你解题的方法是否正确。
allOddBits
/*
1. allOddBits - return 1 if all odd-numbered bits in word set to 1
2. where bits are numbered from 0 (least significant) to 31 (most significant)
3. Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
4. Legal ops: ! ~ & ^ | + << >>
5. Max ops: 12
6. Rating: 2
*/
int allOddBits(int x) {
int mask = 0xAA;
int byte0= mask;
int byte1 = mask << 8;
int byte2 = mask << 16;
int byte3 = mask << 24;
mask = byte0 + byte1 + byte2 + byte3;
x = (x & mask) ^ mask;
return !x;
}
这个比较简单,想法来自于:通过和掩码做AND运算,可以得到你想要的位置上的数字。所以这个函数的解题思路就很明确了:
- 制作掩码
- 拼合掩码
- 做AND运算
- 按要求返回结果
negate
int negate(int x) {
// For all number: Inverse and Increase 1.
// Notice: negate(Tmin) = Tmin
int ans = ~x + 1;
return ans;
}
解题思路非常明确,按教授上课所讲:对一个数字先取反,后加1。就可以得到这个数字的相反数。
有两个数字值得我们注意:
- Tmin(0x80000000): negate(Tmin) = Tmin
- 0(0x00000000): negate(0) = 0
isAsciiDigit
/*
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* isAsciiDigit(0x80000000) = 0
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
int isAsciiDigit(int x) {
// Tail 8 bits: 0011,0000 ~ 0011, 1001
int lowerBound = !((x + (~0x30 + 1)) >> 31); // 1 if x >= 0x30, 0 otherwise
int upperBound = !((0x39 + (~x + 1)) >> 31); // 1 if x <= 0x39, 0 otherwise
return lowerBound & upperBound; // 1 if x is within the range, 0 otherwise
}
想法是考虑:
- x - 0x30 >= 0
- 0x39 - x >= 0
若同时满足上述两个条件,则可确定x是AsciiDigit。
即使x的数值比较极端,在运算是发生了溢出,同样可以正确返回结果。因为无论是0x30 还是0x39,离极值都很远。就算溢出,其运算结果也不会落在0x30与0x39之间。
conditional
/*
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
int conditional(int x, int y, int z) {
int not_x = !x;
int var1 = not_x << 31;
int var2 = var1 >> 31;
int first_num_mask = ~var2;
int second_num_mask = var2;
int first_num_select = first_num_mask & y;
int second_num_select = second_num_mask & z;
return first_num_select | second_num_select;
}
x? y : z的意思等价于如下C语言表达式:
x != 0? y : z
思路就是根据x的值制作掩码Mask
当x != 0 时,对y的mask为0xFFFFFFFF,对z的mask为0x00000000
当x == 0 时,对y的mask为0x00000000,对z的mask为0xFFFFFFFF。
isLessOrEqual
/*
1. isLessOrEqual - if x <= y then return 1, else return 0
2. Example: isLessOrEqual(4,5) = 1.
3. Legal ops: ! ~ & ^ | + << >>
4. Max ops: 24
5. Rating: 3
*/
int isLessOrEqual(int x, int y) {
int sign_x = x >> 31;
int sign_y = y >> 31;
// If sign_x == 1 && sign_y == 0, return 1
int sign_comp = !!((sign_x ^ sign_y) & sign_x);
// If sign_x == sign_y, judge the Expr: x - y <= 0:
int is_equal = !(x ^ y);
// In diff, only sign_x == sign_y will be calculated.
int diff = !!((x + ~y + 1)>>31 & !(sign_x ^ sign_y));
return sign_comp | is_equal | diff;
}
易错点:
像 isAsciiDigit函数中直接使用x - y <= 0来判断x<=y。
很显然这样是不妥的,如果x,y均为负数,且有x < y ,(x - y)发生负溢出变为正数,反而(x - y)离谱地得出了x > y的结果。 因此这里我给出的解决方式是考虑如下三种情况:
- x为负数,y为正数,即x的符号位为1, y的符号位为0。
- x == y
- x,y 符号相同,(x - y)所得结果为负数(即符号位为1)。
小于等于的情况一定为这三种之一。最后运算结果取 | OR运算即可。
logicalNeg
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int logicalNeg(int x) {
return ((x | (~x + 1)) >> 31) + 1;
}
本题要求不使用 ! 非运算完成逻辑上的取非。
这个题当时想了较长时间T_T。但是参考了别人的答案后发现原来挺简单的。
对于不是0的数,和自己的相反数进行 | OR运算后一定可以保证最高位为1.在本次Lab的注意事项中有言:“所以的右移运算都是算数右移,即负数右移补1”。所以右移31位后的值为0xFFFFFFFF, 再+1后即为0。
对于0来说,对0求相反数还是0,OR运算后右移再+1后结果为1。
实现了这道题的目的。
howManyBits
/* howManyBits - return the minimum number of bits required to represent x in
1. two's complement
2. Examples: howManyBits(12) = 5
3. howManyBits(298) = 10
4. howManyBits(-5) = 4
5. howManyBits(0) = 1
6. howManyBits(-1) = 1
7. howManyBits(0x80000000) = 32
8. Legal ops: ! ~ & ^ | + << >>
9. Max ops: 90
10. Rating: 4
*/
int howManyBits(int x) {
int b16, b8, b4, b2, b1, b0;
int sign_mask = x >> 31;
x = x ^ sign_mask;
// From left to right , try to find the first 1.
b16 = !!(x >> 16)<<4;
x = x >> b16;
b8 = !!(x >> 8) << 3;
x = x >> b8;
b4 = !!(x >> 4) << 2;
x = x >> b4;
b2 = !!(x >> 2) << 1;
x = x >> b2;
b1 = !!(x >> 1) << 0;
x = x >> b1;
b0 = x;
return b16 + b8 + b4 + b2 + b1 + b0 + 1;
}
先解释一下题意:
要求返回表示该数字最少需要的比特数。
比如说:-5的二进制表示为 1011。注释中所给答案说-5最少需要4位来表示;
-4的二进制表示为 1100。注释中所给答案说-4最少需要3位来表示;
对于负数来说,若其二进制表示的高位有若干个1。则可以只保留一个1。这是正确的。
对于-4 ,你可以表示为100,最高权重为 -1 * 23-1 = -4
这个的思路没有很难:
先构造一个Maks,然后让输入x与Mask做 ^ XOR运算。之后对上述处理得到的值的二进制表示中,找到从左向右数第一个1。然后经过简单的处理就可以得到正确的结果。
这个题难的地方在于找到首个1出现的位置。难点在于代码实现。笔者给出的代码仅供参考。
方法大致为:
- 先在高16为中找是否有1,若有,则至少有16个比特用于表示该数。然后执行x >> b16。
- 在低16位中的高8位中找1,若有, 则至少又有8个比特用于表示该数。然后执行x >> b8。
- 以此类推,直到考虑了所有比特。
这个方法十分的巧妙啊!
浮点数知识补充
为顺利完成下面的作业,先补充单精度浮点数IEEE754格式的相关内容。
浮点数格式为:
内容 | 符号位 | 指数位 | 尾数位 |
---|---|---|---|
所占位数 | 1 | 8 | 23 |
符号位(MSB):0表示正数,1表示负数
指数位(EXP):无符号8bit数,范围从0 ~ 255。
尾数位(FRAC):表示小数点后的数字。
浮点数计算公式虽着规格化与非规格化的变化而不同,在下面的介绍中我们讨论浮点数计算公式。
IEEE754 下的规格化表示与非规格化表示
规格化
当指数位表示的数既不是0,也不是255时,该浮点数即为规格化小数(Normalized Float)
换言之就是表示EXP域的8位既不是0x00 也不是0xFF。
此时浮点数的计算公式为:
(-1)MSB x (1.FRAC) x 2 EXP-bias
在规格化表示中,bias的计算公式为
2w-1 -1 , 其中w为EXP域的比特位数
比如在单精度浮点数格式中,EXP域位数为8,bias为28-1 -1 = 127
非规格化
当指数位表示的数为0 或为255时,该浮点数即为非规格化小数(Denormalized Float)
当EXP指数域为0xFF时,需要讨论两种情况:
- 若此时FRAC尾数域为全0。则此时该浮点数表示Infinity 无穷。结合符号位来看,若已知在无穷的情况下,符号位为0则表示正无穷,符号位为1则表示负无穷。
- 若此时FRAC尾数域为不为0的数,则此时表示NaN(Not a Number)
当EXP指数域为0x00时,需要讨论两种情况:
-
若此时FRAC尾数域为全0,则表示数字0。结合符号位来看,有+0和-0之分。
-
若此时FRAC尾数域为非0,可以根据如下公式计算该浮点数的值:
(-1)MSB x (0.FRAC) x 2 1-bias
floatScale2
/*
* floatScale2 - 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
*/
// For Float, 1 bit MSB | 8bit EXP | 23bit Frac
unsigned floatScale2(unsigned uf) {
int MSB, EXP_Mask, FRAC_Mask;
int FRAC_Value, EXP_Value;
int NormalValue;
MSB = (!!(uf >> 31)) << 31;
FRAC_Mask = (0xFF << 15) + (0xFF << 7) + 127;
EXP_Mask = (0xFF << 23);
FRAC_Value = FRAC_Mask & uf;
EXP_Value = (EXP_Mask & uf) >> 23;
if(FRAC_Value && EXP_Value == 0xFF) {
// Case for NaN
return uf;
}
else if(!FRAC_Value && EXP_Value == 0xFF) {
// Case for Infinity
return uf;
}
else if(!FRAC_Value && !EXP_Value) {
// Case for +0 / -0
return MSB;
}
else if (!EXP_Value && FRAC_Value) {
return MSB + (FRAC_Value << 1);
}
else {
// Case for normalized float number
EXP_Value = (EXP_Value + 1) << 23;
NormalValue = MSB + EXP_Value + FRAC_Value;
return NormalValue;
}
}
有了前面对浮点数格式的整理,这个函数的实现也就不难完成了。我这里的实现稍微比较乱,读者见谅T_T。
floatFloat2Int
/*
* floatFloat2Int - Return bit-level equivalent of expression (int) f
* for floating point argument f.
* Argument is passed as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point value.
* Anything out of range (including NaN and infinity) should return
* 0x80000000u.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
int floatFloat2Int(unsigned uf) {
int s_ = uf>>31;
int exp_ = ((uf&0x7f800000)>>23)-127;
int frac_ = (uf&0x007fffff)|0x00800000;
if(!(uf&0x7fffffff)) return 0;
if (exp_ >= 31) return 0x80000000;
if (exp_ < 0) return 0;
if(exp_ > 23) frac_ <<= (exp_ - 23);
else frac_ >>= (23 - exp_);
if(s_) {
return ~frac_ + 1;
}
else {
return frac_;
}
}
}
这里参考了这篇知乎博文的写法
首先要做的还是分别利用Mask掩码获得符号位,EXP指数域的值,FRAC尾数域。
算法的思路如下:
- 如果uf是0,return 0;
- 若指数(避免混淆,这里强调是减去bias的值,即代码段中变量exp_)>= 31, 则一定会超出Int类型所能表示的范围。
- 若exp_ < 0 则可知该浮点数一定是个小于1的数,对于小于1的数强制转换为int类型后,结果为0。同时,这步判断也处理了非规格化中EXP域为0x00的情况。
- 进行移位的处理
- 根据原浮点数的符号,返回不同的值。
floatPower2
/*
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
*
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return
* 0. If too large, return +INF.
*
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatPower2(int x) {
int INF = 0xFF<<23;
if (x <= -150) return 0;
else if(-149 <=x && x <= -127) {
return 0x01 << (x + 149);
}
else if(-126 <= x && x <= 127) {
return (x + 127) << 23;
}
else return INF;
}
这个算是比较简单的一个,根据指数不同的范围分类讨论即可。需要注意当指数范围不同时,心里一定要清楚浮点数此时是处于规格化的表示状态中还是非规格化的表示状态中。