CSAPP——Lab1——Data Lab

本篇博客是《深入理解计算机系统》实验记录的第一篇,主要记录实验过程和心得。
实验名为DataLab,对应于书本的第二章:信息的处理与表示。
关于实验的方法请自行阅读实验文件压缩包中的README文件和代码文件中的前缀注释。

Q1

//1
/* 
 * 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 Xor = ~(~x & ~y) & ~(x & y);
  return Xor;
}

Q1分析
题目中要求只是用取反~和与&运算符来实现异或运算^。首先可以列出异或运算的真值表:

x y x^y
0 0 0
0 1 1
1 0 1
1 1 0

根据离散数学里的知识,可以通过真值表将x^y使用与(∧)、或(∨)、非(对于二进制数而言也就是取反~)

x^y = (x ∨ y)(~x ∨ ~y) // 第一行和第四行的和之积表达式
	= ~(~x ∧ ~y)~(x ∧ y) // 德摩根律 (1.1)

将上述表达式1.1翻译为C代码即可。

Q2

/* 
 * tmin - return minimum two's complement integer 
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 4
 *   Rating: 1
 */
int tmin(void) {
    
    
  return 1 << 31;
}

Q2分析:
这道题要求使用! ~ & ^ | + << >>运算符来实现最小补码值,最小的补码值(32位机器上)是:
1000 0000 0000 0000 0000 0000 0000 0000
也就是简单的将1向左移位31位到符号位即可。

Q3(*)

//2
/*
 * 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) {
    
    
 	// return !((1 << 31) + x + 1); 错误的示例
 	int i = x + 1;    /*Tmin或者0*/
  	x = x + i;        /*如果x是Tmax,那么x此时为-1*/
  	x = x + 1;        /*如果x是Tmax,那么x此时为0*/
  
  	i = !i;           /*排除x == -1的情况*/
  	x = x + i;        /*如果x == -1, i此时为1, 会对结果造成影响*/
  	return !x;
}

Q3分析:
上面注释了一个错误的示例,是我第一次写这道题时候的思路,后来在用dlc检查语法时报了错误(不允许使用<<,真是粗心啊)。然后就是重新开始解这道题,我注意到如果这样做了就会发现很难绕开-1这样一个特殊值,于是经常会得到这样的错误:
在这里插入图片描述
所以这道题最终的思路是要剔除-1这样一个例外值的干扰。完整思路如下:

	//如果想证实一个数一定是Tmax,那么可以借助Tmax + 1 = Tmin来确定,它们是互为充要的。
	Tmax + 1 = Tmin
	=> Tmax - Tmin + 1 = 0
	=> Tmax + (-Tmin) + 1 = 0
	=> Tmax + Tmin + 1 = 0	//(3.1)
	
	很多做法都是使用Tmax + Tmin + 1 = 0来判断的,但这里存在一个问题:Tmin如何表示?
	Tmin = Tmax + 1, 但是并不能保证x = Tmax。
	如果一厢情愿的将x当作Tmax来解,就会产生一个增根-1。
	见3.1式,假设我们一厢情愿的去做,3.1式就变成了
	x + (x + 1) + 1 = 0
	x = -1时该式同样成立,此为增根来源,在解答时应该排除之。
	

所以这道题中i = !i,也就是为了对计算结果取反,如果x真的为-1,i就为0,!i一定为1。这样的值会对结果x + i产生干扰,从而作为条件返回。

Q4

/* 
 * allOddBits - return 1 if all odd-numbered bits in word set to 1
 *   where bits are numbered from 0 (least significant) to 31 (most significant)
 *   Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 12
 *   Rating: 2
 */
int allOddBits(int x) {
    
    
  /*首先获取0xAAAAAAAA作为此问题的掩码, 0xAA作为常数值是可以直接使用的*/
  int i = (0xAA << 8) | 0xAA;
  int j = (i << 16) | i;     /*j == 0xAAAAAAAA*/  
  
  return !((x & j) ^ j);	 /*x & j保留了所有奇数位的值,与j异或可以看出是否与j相等*/
}

Q4分析:
这道题的关键在于找出掩码值0xAAAAAAAA,这个数字很特别,它保留了所有奇数位。二进制展示如为:0b 10101010101010101010101010101010。解题思路就是:
1.先构造出这样一个掩码值;
2.使其与x相与来获取x奇数位的对应情况;
3.通过与掩码值异或来判断x对应奇数位是否全为1。
从这个题中大致可以发现这样一种思路引导,也就是当一个问题需要数字中某些特殊位时(比如本题是保留所有奇数位),下手点往往就是先找特殊掩码再进行判断。

Q5

/* 
 * negate - return -x 
 *   Example: negate(1) = -1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 5
 *   Rating: 2
 */
int negate(int x) {
    
    
  return (~x + 1);
}

Q5分析:

这道题是求一个数的逆元,也就是-x,简单总结一下求法:
1.CSAPP课本上的解法:
A.如果x是Tmin,那么x的逆元就是自身Tmin,即Tmin + Tmin = 0
B.如果x不是Tmin,那么x的逆元就是-x,因为这时的取值范围关于0是对称的

这是一种非常直白的理解方法,但是它对本题没有指导作用。
2.位运算解法:
对任何补码,-x = (~x + 1),逆元的位的快速求解就是按位取反再加1,这是一个非常重要的结论。

Q6

//3
/* 
 * isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
 *   Example: isAsciiDigit(0x35) = 1.
 *            isAsciiDigit(0x3a) = 0.
 *            isAsciiDigit(0x05) = 0.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 15
 *   Rating: 3
 */
int isAsciiDigit(int x) {
    
    
  int j = !(x ^ 0x38);          /*特殊值0x38*/
  int k = !(x ^ 0x39);          /*特殊值0x39*/

  /*
    0 0 0 0
    0 0 0 1
    0 0 1 0
    0 0 1 1
    0 1 0 0
    0 1 0 1
    0 1 1 0
    0 1 1 1
    只需要保证第三位为0
  */
  x = !((x >> 3) ^ 0x6);
  return x | j | k;
}

这道题使用位运算来判断某个值是否属于特定范围。
如果将满足条件的数字的二进制位全部列出来,如下所示:

0011 0000
0011 0001
0011 0010
0011 0011
0011 0100
0011 0101
0011 0110
0011 0111
0011 1000
0011 1001

Q6分析:
从表中可以清楚地看出,除了最后两行0x38,0x39以外,其他范围内的数值第3位均为0,且0~2位包含了所有可能的情况。这意味着它们可以被归并为一类,而将剩下的两个作为特例来处理,变量j、k就分别验证了0x38、0x39这两个特例的情况,取反是为了将其当作条件来使用。剩下的就是将原值x右移三位,此时应该确保剩下来的值是0x6(110),这对应了剩下的情况。最终返回值为三个条件的或,意思是满足了其中一条即可。

Q7

/* 
 * 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) {
    
    
  /*注意异或运算的一个性质,x ^ y ^ y = x*/
    int i = !(x ^ 0);               /*测试x是否是0*/
    
    int m = ~(i) + 1;               /*求逆元,1的时候返回-1,0的时候还返回0*/
    int n = ~(!i) + 1;              /*与-1相与时会保留原值,与0相与会得到0*/  

    return y ^ z ^ (m & y) ^ (n & z);
}

Q7分析:
一道颇有趣味的题,上面是我自己的解法。这道题在不允许使用条件判断的情况下实现选择,确实颇费头脑。主要的性质是异或运算的一个性质,即x ^ y ^ y = x,一个数与同一个数连续异或两次,会再次得到自身,有时候也会使用此性质进行加密,其中y称为密钥。

回到这道题,我的想法就是直接将y和z异或起来,通过条件来控制解密的密钥到底是y还是z,从而对应的实现将y或z屏蔽。首先使用一个变量i将x是否为0的条件判断结果存储起来,然后直接使用m、n分别对i,-i进行取反加一,也就是直接取其逆元。因为i和-i总有一个是1,另一个是0,1的逆元是-1,0的逆元是0,任何数与-1相与总能得到自身,而与0相与会变成全0。至此,我们有了结果的基础。

最终返回结果为y ^ z ^ (m & y) ^ (n & z),自己举例测试就可以知道,m&y与n&z总有一个为0,另一个就是想要的掩码。

Q8

/* 
 * isLessOrEqual - if x <= y  then return 1, else return 0 
 *   Example: isLessOrEqual(4,5) = 1.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 24
 *   Rating: 3
 */
int isLessOrEqual(int x, int y) {
    
    
  int sx = !((x >> 31) + 1);      /*为0表示符号位为0,为1表示符号位为1*/
  int sy = !((y >> 31) + 1);
  /*情况1.1:异号 sx = 1, sy = 0*/
  int c1 = (sx & (!sy));

  /*情况1.2:异号 sx = 0, sy = 1,这个不可以成立*/
  int c2 = ((!sx) & sy);

  /*情况2:同号,用x - y是否<=0判断*/
  int i = (~y) + 1;         /*求出y的逆元*/
  int j = x + i;            /*x + (-y) = x - y*/

  /*接下来判断j是否小于等于0*/
  int m = !j;                /*如果j == 0, m一定为1*/
  int n = !((j >> 31) + 1);  /*如果j < 0,那么n一定为1*/

  return c1 | ((!c2) & (m | n));
}

Q8分析:
这道题要求使用指定运算符完成是否小于等于的判断,我是通过分类讨论来做的,也就是对符号位进行讨论。在x y同号时(同大于0或同小于0时),我们可以放心的进行x-y操作,这样确保不会因为溢出而导致结果的符号错误,因此这两种情况可以合并为一种情况,即情况2。另外两种情况就是异号的情况,这两种情况下大小是容易判断的。

sx sy 结果
0 0 情况2,同号,使用x-y是否小于等于0来判断
0 1 情况1.2,异号,x > y一定成立
1 0 情况1.1,异号,x < y一定成立
1 1 情况2,同号,使用x-y是否小于等于0来判断

具体到代码实现时,c1即代表着情况1.1(x < 0, y > 0),c2代表着情况1.2(x > 0, y < 0)。显然c1是符合条件的,c2不符合条件但它却是统一讨论情况2的障碍,所以最终在返回条件中一定要有!c2来将情况1.2排除掉,再与两种同号条件相与。

Q9

//4
/* 
 * 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) {
    
    
  int nx = (~x + 1);      /*得到-x*/

  /*如果x不为0,那么x与-x相或之后符号位一定为1*/
  int i = x | nx;

  /*只需要判断i最高位是否为1就可以了*/
  return (i >> 31) + 1;
}

Q9分析:
这道题要求采用指定的运算符实现逻辑非,即对于0值返回1,非0值返回1。
要解决此问题,一个很重要也稍微困难的事情是要敏锐的察觉到一个条件,可以将0和非0值完全区分开来,这个条件就是:
对于任何一个非零数x,当x != Tmin时,x与-x的符号位一定是异号的,这样它们相或的结果一定可以保证符号位为1。特殊的,当x == Tmin时,x与-x均为Tmin,符号位均为1,相或结果也为1。这样我们证明了对于非零数x,-x | x一定可以保证符号位为1而对于0,这样的结论是不成立的,这就找到了能够完全区分0值和非0值的一个清晰的边界。
接下来就是将上述思路转换成代码了,问题描述很简单,其实解决过程还是有些难度的。

Q10

/* howManyBits - return the minimum number of bits required to represent x in
 *             two's complement
 *  Examples: howManyBits(12) = 5
 *            howManyBits(298) = 10
 *            howManyBits(-5) = 4
 *            howManyBits(0)  = 1
 *            howManyBits(-1) = 1
 *            howManyBits(0x80000000) = 32
 *  Legal ops: ! ~ & ^ | + << >>
 *  Max ops: 90
 *  Rating: 4
 */
int howManyBits(int x) {
    
    
  /*获取数字的符号位, 如果符号位为1,那么Sign也是1*/
  int Sign = x >> 31;

  /*如果是负数那么按位取反,如果是正数那么保持原值*/
  x = (Sign & ~x)  | (~Sign & x);

  /*接下来就是寻找数字中最高位的1在哪里*/
  /*  
    以第一个式子为例,如果判断高16为仍有1存在,
    那么低16位必须保留,不用考虑它们了,直接移掉
  */
  int High16 = !!(x >> 16) << 4;
  x = x >> High16;

  int High8 = !!(x >> 8) << 3;
  x = x >> High8;

  int High4 = !!(x >> 4) << 2;
  x = x >> High4;

  int High2 = !!(x >> 2) << 1;
  x = x >> High2;

  int High1 = !!(x >> 1); 
  x = x >> High1;

  int High0 = x;
  return High16 + High8 + High4 + High2 + High1 + High0 + 1;
}

Q10分析:
这题没有做出来,到网上找的题解,简单阅读了一下代码之后,发现确实很巧妙。首先要搞清楚基本思路:要搞清楚一个数补码最少需要多少位,必须得找出来正数中位于最高位的1在哪里,这一点很好理解。如果是负数,就要找负数中最高位的0在哪里,然后再+1(表示符号位)。
这一点比较难想,大概可以这样理解,负数的负权重全部是由最高符号位贡献的,越大的负数越容易使用较小的位数表示,如果我们想表示更小的负数就必须增加位数,同时保证符号位为1,在负数的二进制表示中出现的最高位0是一种标志,意思是这里的正权重不用了,那么也可以确定表示此数无需更多的二进制位。下面开始解释代码:

 x = (Sign & ~x)  | (~Sign & x);

上面这行代码将正数保持原值,负数按位取反,这样是为了将上面的两种情况结合起来。这时,后面的代码只需要寻找数字中最高位的1即可。后面就是连续的移位判断来判断数字中最高位的1在哪里出现。这里遵循的原理很简单,就是如果一个数位是1,那么比它低的位一定需要保留,所以我们干脆将低位全部移掉(反正它们是一定要保留的),以高16位为例,如果它不是全0,那么低16位一定要保留等等。之所以选2的幂次进行移位,就是因为这样的移动位数方便实现,不需要复杂的组合。

Q11

//float
/* 
 * 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
 */
unsigned floatScale2(unsigned uf) {
    
    
 /*提取此浮点数中的各部分*/
  unsigned Sign = uf >> 31;             /*符号位,注意无符号数执行逻辑右移*/
  unsigned Exp = (uf >> 23) & 0xff;     /*指数部分*/
  unsigned Frac = uf & 0x7fffff;        /*小数部分*/
 
  /*判断parameter是否为NaN,即Exp全1且Frac不等于0*/
  if(Exp == 0xff && Frac != 0)
    return uf;
  
  /*返回值*/
  unsigned Result = 0;
  
  if(Exp == 0)   // 非规格化数
  {
    
    
    /*乘以2首先尝试将小数部分frac左移一位,查看是否有进位到exp*/
    int Carry = (Frac << 1) >> 23;
    Frac = (Frac << 1) & 0x7fffff;  // 移位之后的Frac部分

    Exp = Exp + Carry;
  }
  else if(Exp != 0xff)        // 如果是规格化数且不是无穷大 
    Exp = Exp + 1;
  
  /*将数字重组回去*/
  Result = (Sign << 31) | (Exp << 23) | Frac;
  return Result;
}

Q11分析:
可以稍微松一口气了,浮点数的第一题,因为放宽了编程限制,我们手头的可以使用的工具更多了,这题要求将给定的浮点数乘以2,但是操作是在一个32位的无符号整数上进行的。思路其实很简单,就是分类讨论:规格化数和非规格化数,规格化数里面NaN和无穷大的情况需要摘出来。

1.当数字是非规格化数字时,只需要将Frac部分左移一位即可,如果有进位,那么就实现了从非规格化数到规格化数的平滑过渡。
2.当数字是规格化数字时,因为Frac部分需要加上一个隐式的1,所以单凭左移一位无法获得对应的2倍关系,这时候只需要直接在Exp部分+1即可,但是要排除无穷大的情况,无穷大返回自身即可。NaN直接在进行运算之前直接剔除掉。

Q12

/* 
 1. floatFloat2Int - Return bit-level equivalent of expression (int) f
 2.   for floating point argument f.
 3.   Argument is passed as unsigned int, but
 4.   it is to be interpreted as the bit-level representation of a
 5.   single-precision floating point value.
 6.   Anything out of range (including NaN and infinity) should return
 7.   0x80000000u.
 8.   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 9.   Max ops: 30
 10.   Rating: 4
 */
int floatFloat2Int(unsigned uf) {
    
    
  int Result = 0x80000000;

  /*抽取浮点数中的各部分*/
  unsigned Sign = uf >> 31;             /*符号位,注意无符号数执行逻辑右移*/
  unsigned Exp = (uf >> 23) & 0xff;     /*指数部分*/
  unsigned Frac = uf & 0x7fffff;        /*小数部分*/

  unsigned Bias = 127;                  /*单精度浮点数的固有偏置*/

  /*如果是NaN或者无穷大*/
  if(Exp == 0xff)
    return Result;
  
  /*非规格化数取整一定是0*/
  if(Exp == 0)
  {
    
    
    Result = 0;
    return Result;
  }
    

  /*规格化数的情况*/
  else
  {
    
    
    /*观察还需要左移多少位,负数则右移*/
    int Difference = Exp - Bias - 23;
    int Bound = 7;            /*左移超过7位就会out of bound*/

    /*加上规格化数中隐含的1*/
    Frac = Frac | (1 << 23);
    if(Difference >= 0)      /*左移*/
    {
    
    
      if(Difference <= Bound)
        Frac = Frac << Difference;
      else    /*out of bound*/
        return Result;
    }
    else					/*右移*/
    {
    
    
      Difference = ~Difference + 1;       /*取负*/
      if(Difference < 24)                 /*最多24位有值*/
        Frac = Frac >> Difference;        /*右移*/
      else
        Frac = 0;
    }
  }

  /*无论正负数,我们此时得到了浮点数的整数部分绝对值*/
  Result = 0 | Frac;
  if(Sign)                /*如果浮点数是负数*/
    Result = ~Result + 1;

  return Result;
}

Q12分析:
这道题显得代码有点长(可能是我的问题),但是它的解决思路并不难。这道题要求实现从单精度浮点数float到int的强制类型转换,我们只需要先求出浮点数的整数部分,放到一个整数int的0~30位即可(因为MSB是符号位)。所以需要分几种情况讨论一下:

1.首先是infinity或NaN,这种情况下直接返回要求的值0x80000000即可。
2.非规格化数取整一定是0,也很简单
3.规格化数稍微复杂,解答思路就是无论正负浮点数,如果在int表示范围内,那么我们保留其绝对值。负数在最后进行取反即可,在处理时注意越界问题,考虑到int只有31位可以用,而规格化数加上隐含的1和Frac部分已经有24位,故左移超过7位就会out of bound,右移超过24位一定会得0。

Q13

/* 
 * 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) {
    
    
    unsigned Result = 0;

    /*如果数字很小,超过了最小的非规格化数*/
    if(x < (1 - 127 - 23))
      return Result;

    /*较小的数字,但尚可以非规格化数来表示, 2 to the -126是第一个规格化数*/
    else if(x >= (1 - 127 - 23) && x < -126)
    {
    
    
      /*计算Frac部分的1应该在哪一位*/
      unsigned Difference = -126 - x;
      Result = 1 << (23 - 1 - Difference);
      return Result;
    }

    /*可以使用规格化数表示的情况*/
    else if(x >= -126 && x <= 254 - 127 + 1)
    {
    
    
      /*转换x为对应的Exp部分*/
      x = x + 127;
      Result = x << 23;
    }
    /*超过了最大的浮点数*/
    else
      Result = 0xff << 23;
    return Result;
}

Q13分析:
这道题要求求2的x次幂的浮点数表示,关键在于找准每种情况下(规格化数、非规格化数)的表示范围:

  1. 小于了最小的非规格化数,最小的非规格化数为0 00000000 00000000000000000000001,也就是2的-149次幂,小于这个数返回0。
  2. 在非规格化数的表示范围内,那就是大于等于-149次幂,但是小于最小的规格化数即2的-126次幂。
  3. 在规格化数的表示范围内,就是大于等于-126次幂,小于等0 11111110 11111111111111111111111,即2的+128次幂。因为是2的整数次幂,所以可以知道Frac部分一定全部是0,只需要求Exp部分即可。
  4. 超过了最大的规格化数,这时候就返回infinity即可。

终于做完了,data lab作为csapp的实验,还真是很难。这些问题中Q10不是自己做出来的,感觉很遗憾。这些问题的解决思路也真是非常不寻常,很多与日常的编程思维相差甚远,不过能够从位的角度来认识和学习计算机中数字的表示,也算是一种独特的体验,就把这些题当作一些智力游戏吧。

猜你喜欢

转载自blog.csdn.net/zzy980511/article/details/120608839