CSAPP DataLab

本文详细介绍了使用位操作符实现一系列计算任务,包括:位与、提取字节、逻辑移位、位计数、非运算、最小两补数表示、位数适配、除以2的幂、求负、判断正负、小于等于比较、对数计算。同时,探讨了单精度浮点数的负数表示、转换和加倍操作。这些算法展示了在有限运算符条件下解决问题的巧妙方法。
摘要由CSDN通过智能技术生成

目录

实验内容

bitAnd

getByte

logicalShift

bitCount

bang

tmin

fitsBits

divpwr2

negate

isPositive

isLessOrEqual

ilog2

float_neg

float_i2f

float_twice

结果


实验内容

每个函数对操作符的种类和数量做出了限制,一般只能使用位运算符,且自定义的数字不能超过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. 每1位为一组,共32组,两两相加(第0组与第1组相加,第2组与第3组相加.......)
  2. 每2位为一组,共16组,两两相加
  3. ......
  4. ......
  5. 每16位为一组,共2组,两两相加

首先说明如何实现两两相加,以第1步为例,即把奇数位与偶数位相加,记一个数字temp1的二进制形式为0101......0101temp1 & 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 操作而不用 运算符。

同样是手动递归 + 分治的方法:

  1. 高16位 按位或 低16位,得到一个新的16位结果,如果结果中有1说明原32位中有1。
  2. 对新的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;
}

结果

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值