csapp-datalab

要求用位运算的方式实现各种运算功能,同时包括一些浮点数和整数的相互转化,难点是很多基本的运算比如加减乘除都不能用了,很多地方的实现会感觉很别扭,但确实从计算机内部角度来说会更快。

bitand

用取反和或代替与,相当于德摩根律

/* 
 * bitAnd - x&y using only ~ and | 
 *   Example: bitAnd(6, 5) = 4
 *   Legal ops: ~ |
 *   Max ops: 8
 *   Rating: 1
 */
int bitAnd(int x, int y) {
  return ~(~x | ~y);
}

getByte

取出一个word(4字节)的某个字节

先把x右移$8n(即n<<3)$位,使目标字节位于最低位,然后用0xFF取出来。

/* 
 * 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) {
  return 0xFF & (x >> (n << 3));
}

logisticShift

逻辑右移,和算术右移唯一的区别在于高位填充0而不是填充符号位。

~(((1 << 31) >> n) << 1)相当于造了一个高n位全是0,其他全是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 
 */
int logicalShift(int x, int n) {
  return (x >> n) & (~(((1 << 31) >> n) << 1));
}

bitCount

挺有难度的一个,原理有点类似于分治或者二分,本质上是不断汇总子问题的解,最终求出原问题的解。

换个角度看待bitCount问题,实际上是把一个数拆成32位个1位,把每一位上的数相加得到的就是1的个数。

但是一位一位加需要加32次,那肯定会超过操作数上限。那么利用分治的思想,我可以先两位两位看,把问题这个数变成16个2位,对于每个2位,直接将这两位相加,此时,每两位上的数代表的就是这两位中1的个数。

同样,可以再把2个2位合成1个4位,也就是把这个数看成8个4位,接着看成4个8位,2个16位,最后看成1个32位,这时的x就是我们要的结果了。

/*
 * bitCount - returns count of number of 1's in word
 *   Examples: bitCount(5) = 2, bitCount(7) = 3
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 40
 *   Rating: 4
 */
int bitCount(int x) {
  int temp, mask1, mask2, mask3, mask4, mask5;
  temp = 0x55 | (0x55 << 8);
  mask1 = temp | (temp << 16);
  temp = 0x33 | (0x33 << 8);
  mask2 = temp | (temp << 16);
  temp = 0x0f | (0x0f << 8);
  mask3 = temp | (temp << 16);
  mask4 = 0xff | (0xff << 16);
  mask5 = 0xff | (0xff << 8);
  x = (x & mask1) + ((x >> 1) & mask1);
  x = (x & mask2) + ((x >> 2) & mask2);
  x = (x & mask3) + ((x >> 4) & mask3);
  x = (x & mask4) + ((x >> 8) & mask4);
  x = (x & mask5) + ((x >> 16) & mask5);
  return x;
}

bang

取反,非0变成0,0变成1。

利用性质,只有0取反加1后发生溢出,结果还是0,也就是最高位是0,对于非0的数,(x | (~x + 1))的最高位一定是1。

/* 
 * bang - Compute !x without using !
 *   Examples: bang(3) = 0, bang(0) = 1
 *   Legal ops: ~ & ^ | + << >>
 *   Max ops: 12
 *   Rating: 4 
 */
int bang(int x) {
  return (((x | (~x + 1)) >> 31) + 1) & 1;
}

tmin

返回最小的有符号数。

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

fitsBits

判断x是否能用n位补码表示。

n位补码能表示的数范围是$[-2^{n-1}, 2^{n-1}-1]$。于是先通过sign判断x的正负,bian = n - 1。

分正负情况讨论,正数情况,!(x >> bian)说明$x<=2^{n-1}-1$。

负数情况,由于~x = -x - 1,于是!(~x >>bian)说明$-x - 1 <= 2^{n-1} - 1$即$x >= -2^{n-1}$

/* 
 * 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
 */
int fitsBits(int x, int n) {
  int sign = (x >> 31) & 1;
  int bian = n + (~0);
  return (!sign & !(x >> bian)) | (sign & !(~x >> bian));
}

divpwr2

计算$x/2^n$,向0舍入。

对于x是正数的情况直接右移就n位就可以,难点在于x是负数的情况。

因为直接右移对应的是向下舍入,但负数情况的向零舍入实际上是向上舍入。所以通过(((1 << n) + ~0) & x)把舍去的部分取出来,判断是否为0,如果非零,那么就给最后的结果补1,即从向下舍入变成向上舍入。

/* 
 * 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
 */
int divpwr2(int x, int n) {
  int sign = (x >> 31) & 1;
  int offset = ~(((1 << n) + ~0) & x) + 1;
  return (x >> n) + (!!offset & sign);
}

negate

取反相反数。取反+1即可,前几个操作就一直在用的。

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

isPositive

判断x是否为正数,转化为判断是否为负数和是否为0。

/* 
 * isPositive - return 1 if x > 0, return 0 otherwise 
 *   Example: isPositive(-1) = 0.
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 8
 *   Rating: 3
 */
int isPositive(int x) {
  return !((x & (1 << 31)) | (!x));
}

isLessorEqual

判断$x<=y$是否成立。

还是分类讨论,如果x为负,y为正,则成立

如果x和y同号,那么保证y-x不会溢出,判断,若y-x>=0,则成立

否则不成立

/* 
 * 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 sig1 = (x >> 31) & 1;
  int sig2 = (y >> 31) & 1;
  int gap = ~x + 1 + y;
  return (!(sig1 ^ sig2) & (!gap |  (!((1 << 31) & gap)))) | (sig1 & !sig2);
}

ilog2

求出整数下的$log_2x$。

其实就是找到最高位的1所在的位,思路是二分查找,先找高16位,如果有1,那么继续在高16位里找,直接把低16位丢掉并给答案加16,否则到低16位里找。

思路还挺简单的,但是在只能用位运算,也不能用条件判断的情况下做就像手脚被绑住一样,还是得多动点脑筋。

/*
 * ilog2 - return floor(log base 2 of x), where x > 0
 *   Example: ilog2(16) = 4
 *   Legal ops: ! ~ & ^ | + << >>
 *   Max ops: 90
 *   Rating: 4
 */
int ilog2(int x) {
  int temp, sign, ans, mask_1bit, mask_2bit, mask_4bit, mask_8bit, mask_16bit;
  ans = 0;
  temp = 0x55 | (0x55 << 8);
  mask_1bit = 0x01; //1
  mask_2bit = 0x03; //2
  mask_4bit = 0x0f; //4
  mask_8bit = 0xff; //8
  mask_16bit = 0xff | (0xff << 8); //16
  temp = x & mask_16bit;
  sign = !!(temp ^ x);
  ans += sign << 4;
  x >>= sign << 4;
  temp = x & mask_8bit;
  sign = !!(temp ^ x);
  ans += sign << 3;
  x >>= sign << 3;
  temp = x & mask_4bit;
  sign = !!(temp ^ x);
  ans += sign << 2;
  x >>= sign << 2;
  temp = x & mask_2bit;
  sign = !!(temp ^ x);
  ans += sign << 1;
  x >>= sign << 1;
  temp = x & mask_1bit;
  sign = !!(temp ^ x);
  ans += sign;
  x >>= sign;
  return ans;
}

float_neg

浮点数取反。

只要判断是否为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
 */
unsigned float_neg(unsigned uf) {
  unsigned exp = uf << 1 >> 24;
  unsigned frac = uf << 9 >> 9;
  if (exp == 255 && frac != 0) return uf;
  return uf ^ (1 << 31);
}

float_i2f

整数转浮点数,需要对浮点数舍入和计算规则有充分的了解。

特判x=0的情况,直接返回0。对于其他情况,$|x|$>=1,所以不需要考虑非规格化浮点数的情况。

首先记录正负情况并把x转换成正数ux(因为-2147482648会爆精度,所以ux必须是unsigned int),求出阶数exp,满足$ux/2^{exp}$的范围在$[1,2)$之间。循环求出exp。同时这个exp也是ux最高位1的位数,所以将ux左移31-x位后,低8位就是将要被舍去的部分。

单精度浮点数精度只有23位,所以当$|x|$较大时肯定会出现舍入的情况,浮点数舍入原则是向偶数舍入。

所以要将低8位拆成tail_1bit+tail_7bit位分别做判断,当恰好为0.5时(tail_1bit = 1,tail_7bit = 0),还要判断舍入后的奇偶性,根据判断结果决定舍入。

正常来说这时候第24位一定是1,并且这个1可以被省去(根据浮点数计算方式)。但是有一个非常细节的地方需要判断,考虑这种非常极端的情况,比如x=2147483647,由于舍入要加一,结果导致这加的一位一直被进位到第25位上。但第25位已经属于阶码位了,这肯定是不允许的。

好在一旦发生这种情况,这意味着低24位一定全是0(只有以上这种不断进位的情况可能导致),否则第24位一定为1。所以只要再把这个数右移一位,然后令阶码+1。这样一来还能保证第24位仍然为1。

而判断这种情况只要判断第24位是否为1,也就是(ux & (1 << 23))是否成立。

最后把符号位和阶码位填上,就得到答案了。

/* 
 * 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
 */
unsigned float_i2f(int x) {
  unsigned ux, sig = (x >> 31) & 1;
  unsigned exp = 0, t;
  unsigned tail_1bit, tail_7bit;
  if (x == 0) return 0;
  if (x < 0) ux = -x;
  else ux = x;
  t = ux;
  while(t) {
    exp++;
    t /= 2;
  }
  exp--;
  ux <<= 31 - exp;
  tail_1bit = ux & 0x80;
  tail_7bit = ux & 0x7f;
  ux >>= 8;
  if (tail_1bit && tail_7bit) ux++; 
  else if (tail_1bit && !tail_7bit && (ux & 1)) ux++;
  if (ux & (1 << 23)) ux ^= (1 << 23);
  else exp++;
  exp += 127;
  ux |= sig << 31;
  ux |= exp << 23;
  return ux;
}

float_twice

浮点数乘二。

对于NaN,无穷大和0,直接返回。

对于规格化浮点数(阶码非0),阶码加一后返回。

对于非规格化浮点数的情况,如果存储位$frac<=4194303=2^{22}-1$,也就是保证乘2后还是一个非规格化浮点数,那么存储位乘二后返回。

否则一定会从一个非规格化浮点数转换成规格化浮点数,因为只需要乘二,说明阶码一定是从0变成1,但这两种情况所对应的实际阶数是一样的,都是E = 1-bias = -126,所以只要保证存储位仍然相等就可以了。区别就在于规格化浮点数最后计算时要加1,那么在存储时减1,反应在存储位上的方法则是减去$8388608=2^{23}$。

/* 
 *
 * 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
 */

unsigned float_twice(unsigned uf) {
  unsigned sign = (uf >> 31) & 1, exp = (uf >> 23) & 0xff, frac = uf << 9 >> 9;
  unsigned ans = 0;
  if (exp == 255 || (exp == 0 && frac == 0)) return uf;
  if (exp != 0) {
    exp ++;
    ans = sign << 31 | exp << 23 | frac;
    return ans;
  }
  if (frac <= 4194303) frac *= 2;
    else {
      exp++;
      frac = frac * 2 - 8388608;
    }
    ans = sign << 31 | exp << 23 | frac;
  return ans;
}