CSAPP datalab

写在前面:出于 hornor code 要求,这些答案本不应出现在这里,但是考虑到这些 lab 已经成型了,不同于 CMU15-445 这种每年的 lab 都会变的变态课,csapp 的 lab 答案早已被解析透了,我这里权当个人记录,而且这个 lab “奇技淫巧”的感觉多一些,认真思考后见识一下答案并没有坏处

CSAPP官网:http://csapp.cs.cmu.edu/3e/labs.html

答案版本1:https://github.com/Exely/CSAPP-Labs

答案版本2:https://www.zhihu.com/column/c_1325107476128473088

答案版本3:https://www.bilibili.com/video/BV183411k7VM?p=4

题目一:bitXor

题目描述:请仅使用 ~ 和 & 实现 ^ 操作

允许的操作符:~ &

允许的最大操作数:14

题目分析:德摩根律的运用

题目解答:

int bitXor(int x, int y) { return ~(~x&~y)&~(x&y); }

题目二:tmin

题目描述:请返回补码表示的最小integer值

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:4

题目分析:integer为32bit,为方便分析,以4bit为例,4bit补码中最小的数为-8,即1000,观察可知是由0001左移3位得到

题目解答:

int tmin(void) { int a = 1; return a << 31; }

题目三:isTmax

题目描述:判断x是否为最大integer值,是的话返回1,否则返回0

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:12

题目分析:integer为32bit,为方便分析,以4bit为例,最大补码值为7,即0111,观察0111有何独特的特征?0111 + 1 的结果和 0111 按位取反的结果是相同的,都是1000,即 (0111 + 1) ^ (~0111) = 0 ,判断两个值相等会经常用到“两数相等异或结果为0”的原理。出于一些对于位操作敏感的嗅觉,还会发现除了0111,1111也具有这个特征,所以还应该排除1111这种情况

题目解答

int isTmax(int x) { //这里双感叹号的操作可以理解为一个规格化的操作,使所有非0的数都为标准的1,使所有为0的数都为标准的0 return !((x + 1) ^ (~x)) & !!(~x); }

题目四:allOddBits

题目描述:判断一个数的奇数位是否为1,是的话返回1,否则返回0

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:10

题目分析:若输入为0xAAAAAAAA,即返回1,以4bit为例,思考1010的结构有什么特点,即如何判断一个数是不是101010...这样的结构,要判断一个数字是不是1010结构,只需要判断这个数和 1010 相 & 的结果是不是 1010 即可,比如1000,他和1010相&的结果是 :1000 & 1010 = 1000,即1000破坏了1010这种结构,得到了1000。

使用 ^ 判断相等,传统艺能了。

作业要求常量不能大于255即0XFF,所以还有一个问题就是如何得到 0xAAAAAAA? 借用移位的妙招,先构造允许的AA,然后左移为AA00,和AA相加,得到AAAA,然后依次循环得到AAAA

题目解答

int allOddBits(int x) { int AA = 0xAA; int AAAA = (AA << 8) + AA; int A8 = (AAAA << 16) + AAAA; return !((x & A8) ^ A8); }

题目五:negate

题目描述:请返回入参x的相反数

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:5

题目分析:相反数,形式很简单,但是背后却有很深的原理,~x+1,顺便一提,以4bit为例,按位取反加1还是自己的数有两个(0000 和 1000)0和最小值-8

题目解答:

int negate(int x) { return ~x + 1; }

题目六:isAsciiDigit

题目描述: return 1 if 0x30 <= x <= 0x39

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:15

题目分析:间接使用减法

题目解答

int isAsciiDigit(int x) { int Tmin = 1 << 31; int diff1 = x + (~0x30 + 1); // x-low int diff2 = x + (~0x3A + 1);// x-high 303A很灵性 //若x-low为非负数(最高位为0)且x-high为负(最高位为1),就是返回1 int res = (!(diff1 & Tmin)) & (!!(diff2 & Tmin)); return res; }

题目七:conditional

题目描述:使用位操作实现三元运算符 x ? y : z

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:16

题目解答

int conditional(int x, int y, int z) { // !!x为 0000 和 0001, pure 变为了 0000 和 1111 int pure = ~(!!x) + 1; //原方案为 !!x + (~1 +1) ,即0000和0001变为1111和0000,为了省一步操作,变成了上面的 return (pure&y)|(~pure&z); }

题目八:isLessOrEqual

题目描述:if x <= y then return 1, else return 0

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:24

题目分析:直接使用y-x反而会复杂,所以分情况讨论

题目解答

int isLessOrEqual(int x, int y) { // y-x // 这个方案有边界bug,对于最小值 1000,-8 ~1000+1结果还是1000,1000 + 1000 得到0000 // 所以增加 | 后面的作为补充这种情况,判断是不是0000,是的话也返回1 //这种减法的方案实现起来有太多边界条件需要考虑,所以需要换一种思路来解决这个问题 //这个不想前面的0x30~0x39的问题有明确的边界,那个不需要考虑边界问题 //减法什么情况下会溢出?在书中学过,是XY不同号的情况下减法会溢出,若xy同号,怎么减都不会溢出的 int signX = (x>>31)&0x1; int signY = (y>>31)&0x1; // x == y con1仅在x=y的情况下返回1 int con1 = !(x ^ y); // x - 最高位1 y + 最高位0,con2仅在 x- y+的情况下返回1 signX & (!signY) int con2 = (signX) & (!signY); // x +最高位0 y -最高位1,con3在仅x+ y-的情况下返回0 !signX & signY int con3 = !((!signX) & (signY)); // x y 同号,con4是x-y的最高位,仅在x<y的情况下返回1 int con4 = (((x + (~y+1))>>31) & 0x1); return con1 | (con2 | (con3 & con4)); }

题目九:logicalNeg

题目描述:实现逻辑“非”,使用除了!之外的一切操作符

允许的操作符:! ~ & ^ | + << >>

允许的最大操作数:12

题目解答

int logicalNeg(int x) { //性质:一个非0数和自己的相反数 (都是补码表示),按位或 的结果最高位一定是1 如5 = 0101 -5=1011 4=0100 -4=1100 int negX = ~x+1; //当x为0时,sign为0000(32bit);当x非0时,sign为1111(32bit) int sign = (negX | x) >> 31; return sign + 1; }

题目十:howManyBits(此题我分析的不好,建议看其他人的)

题目描述:返回最小的数字-可以表示一个int类型数字的补码

思路:

从右边的最低位一直往左找,直到找到最左边的1

/* howManyBits - return the minimum number of bits re to represent x in * two's complement * Examples: howManyBits(12) = 5 0+1100 * howManyBits(298) = 10 0+1 0010 1010 * howManyBits(-5) = 4 1 + 011 * howManyBits(0) = 1 * howManyBits(-1) = 1 * howManyBits(0x80000000) = 32 * Legal ops: ! ~ & ^ | + << >> * Max ops: 90 * Rating: 4 */ int howManyBits(int x) { int isZero = !x; //当x为0时特殊处理 int mask = (!!x << 31) >> 31; int bit16, bit8, bit4, bit2, bit1, bit0; int flag = x >> 31; x = (~flag & x) | (flag & ~x); // 0111 1111 1111 1111 1111 1111 1111 1111 bit16 = (!((!!(x >> 16)) ^ 0b1)) << 4;//16 低位16bit算进去了 x >>= bit16; // 0111 1111 1111 1111 bit8 = (!((!!(x >> 8)) ^ 0b1)) << 3;//8 低位8bit算进去了 x >>= bit8; // 0111 1111 bit4 = (!((!!(x >> 4)) ^ 0b1)) << 2; //4 低位4bit算进去了 x >>= bit4; // 0111 bit2 = (!((!!(x >> 2)) ^ 0b1)) << 1; //2 低位2bit算进去了 x >>= bit2; // 01 bit1 = (!((!!(x >> 1)) ^ 0b1)) << 0; //0 高一半位没1了,所以低一半位不能算进去 bit1=0,合理 x >>= bit1; // x依旧为01 bit0 = x; // x为01 001 0001 均可能 // 最后的+1是符号位 return isZero | (mask & (bit16 + bit8 + bit4 + bit2 + bit1 + bit0 + 1)); }

题目十一:floatScale2

题目描述:入参是unsigned类型的32bit数字,但我们做题人要把这32bit想象成 一个浮点数来对待,因为要求出参为这个浮点数*2 的32bit表示

题目分析:

本题考察对IEEE754标准的是否掌握,需要根据IEEE754的标准,分别取出float中的1bit符号位、8bit解码位、23bit小数位

int floatFloat2Int(unsigned uf) { //取出1bit符号位,这种取法保持了符号位在最高位,方便后续直接使用或运算将符号位还原到最高位 int sign = uf & (1 << 31); int exp = (uf >> 23) & 0xFF; //取出8bit阶码位 int frac = uf & 0x7FFFFF; //取出23bit小数位 }

接下来是最关键的地方,要分情况讨论,至于为什么要分情况讨论,我觉得是出于阅读了IEEE754标准后的直觉、再加上一点聪明的头脑、或者是Google了答案哈哈哈

根据754标准,依据exp的不同,把浮点数划分为:规格化、非规格化、特殊值 三类:

//特殊值,依据题目要求,返回自己 if(exp == 0XFF) { retuen uf; } //非规格化的值 if(exp == 0X0) { retuen uf; } //规格化的值 if(exp != 0X0 && exp != 0xFF) { retuen uf; }

非规格化的值

在非规格化值的情况下,如何表示2*f ,观察浮点数表达式:
$$
V = (-1)^s \cdot M \cdot 2^E
$$
很显然只要在E的基础上加1,最后的结果就是2倍。

以这个数字为例,exp的8bit都是0,所以这是非规格化的浮点数,E = 1 - Bias = -126,M = 0.f = 1/2 ,V = 2^(-127):
$$
0, 0000 0000, 100 0000 0000 0000 0000 0000 = 2^{-127}
$$
E+1后,exp的8bit就是0000 0001 ,导致整个数变为规格化的浮点数,E = e - Bias = -126,M = 1.f = 1.5 ,V = 2^(-126) * 1.5:
$$
0, 0000 0001, 100 0000 0000 0000 0000 0000 = 2^{-126} \cdot 1.5
$$
发现不对了,没有变为2倍,而是变成了1.5倍!所以这启发了我们,对于非规格化的浮点数,若使用E+1这种方案,就需要改变exp的解码,但是exp解码一改变,就变成了规格化的浮点数,这就导致M的解释方法变了(从M=0.f变为M=1.f),最终导致计算出来的V值不是乍看上去的2倍了,所以就不要考虑在2^E项上做手脚了,那就考虑让M变为2倍,M作为一个二进制小数,是可以通过左移变为2倍的

然后检查一下边界情况,如果左移把位数M的最高位1移没了怎么办:(即左移把一个非规格化的值变成了一个规格化的值)
$$
0, 0000 0000, 100 0000 0000 0000 0000 0000= 2^{-127}
$$
左移1bit后变成了规格化的值:
$$
0, 0000 0001, 000 0000 0000 0000 0000 0000= 2^{-126}
$$
这得益于IEEE754标准中规格化的值和非规格化的值之间过渡平滑的特性,所以得到结论,如果是非规格化的值,要使用左移,来求得2*f的32bit正确表达

//非规格化的值 if(exp == 0X0) { retuen (sign | uf << 1); }

规格化的值

对于规格化的值,就可以通过E+1的方式,但要考虑一下边界情况:比如我们很快能想到exp为1111 1110的情况,E+1后,规格化的值左移变成了特殊值,以这个数字为例:

这是规格化的浮点数,E = e - Bias = 127,M = 1.f = 1.0 ,V = 1:
$$
0, 1111 1110, 000 0000 0000 0000 0000 0000 = 2^{127}
$$
E+1后,变成了正无穷:
$$
0, 11111111, 000 0000 0000 0000 0000 0000 = 正无穷
$$
发现也是合理的,因为float的规格化值上限就是(2-δ)* 2^{127}, 所以2^128在float看来就是会溢出为正无穷

//规格化的值 if(exp != 0X0 && exp != 0xFF) { retuen sign | (exp + 1) | frac; }

总结:

int floatScale2(unsigned uf) { //取出1bit符号位,这种取法保持了符号位在最高位,方便后续直接使用或运算将符号位还原到最高位 int sign = uf & (1 << 31); int exp = (uf >> 23) & 0xFF; //取出8bit阶码位 int frac = uf & 0x7FFFFF; //取出23bit小数位 //特殊值,依据题目要求,返回自己 if(exp == 0XFF) { retuen uf; } //非规格化的值 if(exp == 0X0) { retuen (sign | uf << 1); } //规格化的值 if(exp != 0X0 && exp != 0xFF) { retuen sign | (exp + 1) << 23 | frac; } }

题目十二:floatFloat2Int

题目描述:要求返回浮点数f的表达式 (int)f 的等效位级表示。入参是unsigned类型的32bit数字,但我们做题人要把这32bit想象成 一个浮点数来对待,因为要求出参为这个浮点数的转换为int后的值,超过范围的返回 0x80000000u

举个例子,输入为:
$$
0,10000010,00110100000000000000000
$$
这32bit按照IEEE754标准代表浮点数9.625:
$$
V = (-1)^s \cdot M \cdot 2^E = 1\cdot1.001101\cdot2^{130-127} = (1+1/8+1/16+1/64) \cdot2^3 = 9.625
$$
在C语言中,浮点数转整形是向零舍入的,所以
$$
int(9.625) = 9
$$
输出应为:
$$
0000 0000, 0000 0000, 0000 0000,00001001
$$
题目分析

对于绝对值小于1的浮点数,返回的值应该是0

对于绝对值大于1的浮点数,返回的值是这个浮点数的整数部分

按照上一题的思路,尝试进行分类讨论,依旧是先取出三元组:符号位、阶码位、尾数位:

int floatFloat2Int(unsigned uf) { //取出1bit符号位,这种取法保持了符号位在最高位,方便后续直接使用或运算将符号位还原到最高位 int sign = uf & (1 << 31); int exp = (uf >> 23) & 0xFF; //取出8bit阶码位 int frac = uf & 0x7FFFFF; //取出23bit小数位 int E = exp - 127;//这里E = 1-Bias的情况其实会包含在E<0的情况中 }

这道题用IEEE754浮点数公式的“二进制理解”更容易理解,回顾公式:
$$
V = (-1)^s \cdot M \cdot 2^E
$$
这个公式可以理解成将M代表的二进制小数(1.xxx或0.xxx)的小数点左移或者右移,比如:
$$
V = (-1)^0 \cdot 1.0011 \cdot 2^3
$$
就可以解释为将1.0011的小数点往右移动3bit,所以V的值就是$1001.1$,而小数点左边的1001就是我们要的整数答案

所以这题的解题思路就是:想办法得到小数点移位结束之后的、小数点左边的二进制表示的十进制数字(比如上面这个例子中的1001)

分情况讨论的关键在于:E的值,即小数点到底右移、左移几位?

情况1:E < 0,由于M是1.xxx或0.xxx,M表示的二进制小数 小数点左边只有一位,E小于0表示小数点至少往左边移动一位,所以移动完成后,小数点的右边一定是0,即整数部分是0,这种情况下,直接返回0就是要求的答案。

情况2:E>= 32,表示M一定是1.xxx(因为E等于-126时M才是0.xxx),如果E >= 32,表示小数点要往右移动32bit,但是int类型最多32bit,所以会爆掉,按照题目要求返回 0x80000000u

情况3:0 <= E < 32

这里需要先把frac23bit前面的1补上,即$(1<<23) | frac$​​​,这个表达式代表M,如M = 1.11001,则这个表达式就是11100100......000(共24bit)
当E>=23时,表示小数点右移可以把M小数点右边的23bit小数都吃掉,还能给后面继续补0,所以直接返回24bit+(补的几个0) 即可。

当E<23时,表示小数点右移不能把M小数点右边的23bit小数都吃掉,只能吃掉一部分,所以返回24bit右移“割舍的bit”

frac = frac|1<<23; if(E<23) { frac>>=(23 - E);//割舍 } else{ frac <<= (E - 23);//继续补0 }

总结:

int floatFloat2Int(unsigned uf) { int sign = uf & (1 << 31); int exp = (uf >> 23) & 0xFF; //取出8bit阶码位 int frac = uf & 0x7FFFFF; //取出23bit小数位 int E = exp - 127;//这里E = 1-Bias的情况其实会包含在E<0的情况中 frac=frac|1<<23; if(E<0) return 0; if(E >= 31){ return 0x80000000u; } if(E<23) {//需要舍入 frac>>=(23-E); }else{ frac <<= (E - 23); } if(sign) { return ~frac + 1; } else { } }

题目十三:floatPower2

题目描述:要求返回浮点数f的表达式 2.0^x 的等效位级表示。入参是 int 类型的32bit数字,出参是unsigned类型;结果太小的话返回0,太大的话返回+INF

unsigned floatPower2(int x) { return 2; }

题目分析:本题考察浮点数的表示范围。与前面两道题目不同,入参是一个int类型的数字,前两题入参形式上是unsifgned,但是出题人要求我们看成32bit的单精度浮点数float,然后去表达2*f、int(f)等的位级表示;但是此题输入的确就是一个整型,题目要求也是把入参看成整型。但是出参是一个浮点数

先回答一个问题,2.0x次方长什么样,float如何存储2.0x

情况一,都是0:

……

2-150的二进制表示为0|00000000|00000000000000000000000

情况二,阶码0,可以观察到是由1左移获得的,从理论上分析是因为M = 0.000x,E = -126

-------------------------------------------------------------------

2-149的二进制表示为0|00000000|00000000000000000000001

2-148的二进制表示为0|00000000|00000000000000000000010

2-147的二进制表示为0|00000000|00000000000000000000100

……

2-129的二进制表示为0|00000000|00100000000000000000000

2-128的二进制表示为0|00000000|01000000000000000000000

2-127的二进制表示为0|00000000|10000000000000000000000

情况三,阶码非0:

-------------------------------------------------------------------

2-126的二进制表示为0|00000001|00000000000000000000000

2-125的二进制表示为0|00000010|00000000000000000000000

2-124的二进制表示为0|00000011|00000000000000000000000
……

20 的二进制表示为0|01111111|00000000000000000000000

21 的二进制表示为0|10000000|00000000000000000000000

……

2127 的二进制表示为0|11111110|00000000000000000000000

情况四,无穷大:

2^128 的二进制表示为0|11111111|00000000000000000000000

2^129 的二进制表示为0|11111111|00000000000000000000000

看到本题要敏感,2.0的x次幂,正好是IEEE754标准的最后一项:
$$
V = (-1)^s \cdot M \cdot 2^E
$$
只要保证M是1.000......00,s是0,或者(M是0.000x,小数点右边只有一个1),最后的结果就是本题要求的结果,所以尝试把入参x看作最后一项的E来进行思考

image-20220310011508758

单精度浮点数的表示范围如上图所示:

分析题目,2.0x一定是个正数,所以最后的表达式一定大于0,float的符号位一定是0,就从图上零点右边开始分析:

情况1:非规格化值能表示的最小2.0x是2-149,所以x<-149时,float无法表示,直接返回0

情况2:规格化值能表示的最小2.0x是2-126,所以-149 <= x < -126时,是非规格化的值可以表示的,非规格化的值特点是阶码exp为0,E = 1 - Bias = -126,,比如x是-130,$V = 2^{-126} * 2^{-4}$,所以M = 2-4,要返回2-130的float表示就是0 ,0000 0000, 000 01000 00000 00000 00000,可以看到是1左移了19bit,19=23-4

情况3:规格化值能表示的最大2.0x是2127,所以 -126 <= x <= 127时,要使V是2.0x,需要M是1.000......00,这个很简单,直接把exp的值<<23bit即可,exp = E +Bias(E就是x)

情况4:x > 127时,float无法表示了,返回+INF

unsigned floatPower2(int x) { //比float最小值还小,无法使用float表示,return 0 if(x < -149) return 0; //非规格化的数,M=0.000x,E = 1-Bias = -126,exp = 0000 0000 if(x < -126){ int shift = 23 + (x + 126); return 1 << shift; } //规格化的数 if(x <= 127) {//需要舍入 int exp = x + 127; return exp << 23; }else{//比float最大值还大,无法使用float表示,return 正无穷 return 0XFF << 23; } }

__EOF__

本文作者byFMH
本文链接https://www.cnblogs.com/looking-for-zihuatanejo/p/15993893.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   byFMH  阅读(312)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示