CSAPP Lab-1 DATALAB

本文原发于 2023-09-02 15:32:57 于我的 hexo 博客,现迁移至此。

最近看完了 CSAPP 整本书,发现官网上还有 11 次实验可以做。

UPD:好像只有 9 个,因为有两个是旧版本的,可以被新版的替代掉。

UPD:好像只有 8 个,performance 也算是旧的实验了,但是没有明确指出。

Lab 地址:http://csapp.cs.cmu.edu/3e/labs.html

bitXor(x,y)

题意

任务:\(x\) 异或 \(y\)x ^ y

允许使用的运算符:~ &

操作数限制:\(14\)

Solution

对于每一位分别考虑。假设 \(P, Q\) 分别是 \(x, y\) 的某一个对应位,那么这一位上的异或和就是

\[\begin{aligned} P \oplus Q &= \overline{P}Q + P\overline{Q}\\ &= \overline{\overline{\overline{P}Q}\ \overline{P\overline{Q}}} \end{aligned} \]

Code

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

\(8\) 次操作。

tmin()

题意

任务:二进制补码下最小的 int 类型的数

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(4\)

Solution

这个题目非常简单,答案就是 \(-2^{31}\),在 int 补码下是 \(1\) 后面接 \(31\) 个零,只有最高位为 \(1\),也就是 1 << 31

Code

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

\(1\) 次操作。

isTmax(x)

题意

任务:判断 \(x\) 是不是补码表示下最大的 int 类型数。

允许使用的运算符:! ~ & ^ | +

操作数限制:\(10\)

Solution

我的做法比较奇怪吧。

最大的 int 类型数也就是 \(2^{31} - 1\)0x7fffffff)。

如果 \(x\) 满足要求,那么 \(x+1\) 就是 \(-2^{32}\)0x80000000),等于 ~x。可以使用 ~((x + 1) ^ x) = 0 来判断。

但是需要注意的是,\(-1\)0xffffffff)也满足这个性质,因此需要特判这种情况。可以使用 !(x + 1) = 1 来判断。

因此综合起来,用 \(a\) 表示 \(x+1\),最后的答案就是 !((~(a ^ x)) | !a)

Code

int isTmax(int x) {
  int a = x + 1;
  return !((~(a ^ x)) | !a);
}

\(6\) 次操作。

allOddBits(x)

题意

任务:判断 \(x\) 的奇数位是否都是 \(1\)

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(12\)

Solution

这题难住了我不短的时间。

其实是一开始没有想到直接构造出所有奇数位的掩码。

题目限制只能使用 \(0\)\(255\) 之间的整数,所以可以使用 0xAA10101010)扩展两次得到 \(32\) 位的奇数位掩码。

假设 a = 0xAA,那么使用 (a << 8) + a 即可得到 \(16\) 位奇数位掩码,将结果存进 \(a\) 中,再使用 msk = (a << 16) + a 即可得到完整 \(32\) 位的掩码。

使用 x & msk 就是 \(x\) 所有奇数位,使用 ^\(msk\) 进行对比,异或结果为 \(0\) 说明相同,再取非即可得到答案。

Code

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

\(7\) 次操作。

negate(x)

题意

任务:返回 \(-x\) 的补码。

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(5\)

Solution

经典问题,答案就是取反再加 \(1\),也就是 \((~x) + 1\),不解释。

Code

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

\(2\) 次操作。

isAsciDigit(x)

题意

任务:判断 \(x\) 对应的 ASCII 码是不是数字字符(也就是 0x30 到 0x39` 之间)。

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(15\)

题解

首先判断高 \(28\) 位是不是 0x3。使用 !((x >> 4) ^ 3 判断。

然后判断低 \(4\) 位是不是 1001 及以下。考虑到不合法的分别有 1010, 1011, 1100, 1110, 1111 这些数,满足第 \(3\) 位(最低位为第 \(0\) 位)为 \(1\),且第 \(2, 3\) 位至少有一位为 \(1\)。只要这两个条件有一个不满足就可以认为合法。

\(3\) 位不为 \(1\) 可以用 !(x & 8) 判断。

\(2, 3\) 位不存在一位为 \(1\) 可以用 !(x & 6) 判断(6 = 0110)。

Code

int isAsciiDigit(int x) {
  int pre = !((x >> 4) ^ 3);
  return pre & (!(x & 8) | !(x & 6));
}

\(9\) 次操作。

conditional(x, y, z)

题意

任务:返回 C 语言 x ? y : z 的结果。

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(16\)

Solution

假设我们可以把 \(x\) 扩展成一个 \(k\),满足如果 \(x\) 为真,那么 k = 111...1,否则 \(k = 000...0\),那么我们可以把答案表示成 (k & y) | ((~k) & z)

考虑怎么扩展。我们可以用 \(!x\) 得到 \(x\) 的逻辑值的非,只有末位可能为 \(1\),高位全部为 \(0\),作为初始的 \(k\),然后倍增构造。

使用 k = (k << 1) | k 构造两位版本,然后 k = (k << 2) | k 构造四位,以此类推,即可得到完整的 \(k\)

当然,最后得到的 \(k\) 与我们之前假设的相反,为 !x 的扩展,因此答案表达式也需要稍作改变。

Code

int conditional(int x, int y, int z) {
  int k = !x;
  k = k | (k << 1);
  k = k | (k << 2);
  k = k | (k << 4);
  k = k | (k << 8);
  k = k | (k << 16);
  return ((~k) & y) | (k & z);
}

\(15\) 次操作。

isLessOrEqual(x, y)

题意

任务:判断 int 类型下 \(x\) 是否小于等于 \(y\)

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(24\)

Solution

一般来说,只需要判断 \(x - y\) 的正负性即可。\(x - y\) 可以使用 \(x\) 加上 \(-y\) 的补码得到,也就是 x + (~y) + 1。如果 \(x - y = 0\) 需要特殊判断。

但是,如果 \(x - y\) 发生溢出,那么这样判断就会不准确了。因此我们可以先判断 \(x, y\) 各自的符号,只有在同号的时候我们才判断 \(x-y\)。如果 \(x, y\) 符号位不同,可以直接判断结果。

一个数的符号位可以通过 (x >> 31) & 1 获得。

具体地,假设 \(z = x - y\)nx, ny, nz 分别为 \(x, y, z\) 的符号位,f1 表示 \(x - y = 0\) 的逻辑值。那么如果 \(f1\) 为真,或者 \(x\)\(y\) 正(nx & !ny)答案就是 1,此外,除非 nxny 相同(nx ^ !ny)才会需要 \(z\) 的符号位。

结果就是 f1 | (nx & !ny) | ((nx ^ !ny) & z),其中 !ny 重复使用,可以增加一个中间变量来节省操作数。

Code

int isLessOrEqual(int x, int y) {
  int nx = (x >> 31) & 1;
  int ny = (y >> 31) & 1;
  int nny = !ny;
  int z = x + (~y) + 1;
  int f1 = !z;
  z = (z >> 31) & 1;
  return f1 | (nx & nny) | ((nx ^ nny) & z);
}

\(16\) 次操作。

logicalNeg(x)

题意

任务:使用逻辑非,即 !x

允许使用的运算符:~ & ^ | + << >>

操作数限制:\(12\)

Solution

和之前 conditional 这个题中构造 \(k\) 的过程很相似,不是要反过来做。

每次我们把 \(x\) 高位的一半和低位的一半做按位或(|),然后假装 \(x\) 少了一半位,那么当 \(x\) 只剩一位的时候,\(x\) 就是所有位的或了。

那么只需要将末位取出,然后 ^ 1 构造出非。

Code

int logicalNeg(int x) {
  x = (x >> 16) | x;
  x = (x >> 8) | x;
  x = (x >> 4) | x;
  x = (x >> 2) | x;
  x = (x >> 1) | x;
  return (x & 1) ^ 1;
}

\(12\) 次操作。

howManyBits(x)

题意

任务:返回补码形式下,表达 \(x\) 最少需要多少位。

允许使用的运算符:! ~ & ^ | + << >>

操作数限制:\(90\)

Solution

愣是读题读了半天才理解……这里说的补码形式是,一个二进制串最高位为符号位,剩下的使用补码构造。

那么如果 \(x\) 是负数,我们将之取反,得到的结果不变。

然后我们只需要找到最高的一个 \(1\) 的位置,倍增统计即可。

具体地,假设 \(x\) 现在是 \(32\) 位,我们取出最高的 \(16\) 位,判断是否全是 \(0\),如果是的话就将 \(x\) 设置为低 \(16\) 位,否则将答案加上 \(16\) 然后将 \(x\) 设置为高 \(16\) 位。

位数折半,重复以上过程直到最后一位做完。

答案再上加 \(1\) 的符号位即可。

Code

int howManyBits(int x) {
  int f, p;
  int ans = 0;
  int s = x >> 31;
  x = (~s & x) | (s & ~x);
  f = !!(x >> 16), p = f << 4;
  ans = ans + p, x = x >> p;
  f = !!(x >> 8), p = f << 3;
  ans = ans + p, x = x >> p;
  f = !!(x >> 4), p = f << 2;
  ans = ans + p, x = x >> p;
  f = !!(x >> 2), p = f << 1;
  ans = ans + p, x = x >> p;
  f = !!(x >> 1), p = f;
  ans = ans + p, x = x >> p;
  f = x, p = f;
  ans = ans + p + 1;
  return ans;
}

\(37\) 次操作。

floatScale2(uf)

题意

任务:以 unsigned 类型给出一个 \(32\) 位浮点数 \(uf\) 的位级表示,求出 \(uf \cdot 2\) 的位级表示。

允许使用的运算符:所有整数操作符,包括 &&, ||if, while

操作数限制:\(30\)

Solution

首先用下面的代码求出符号、尾数和阶码。

int s = uf & (1 << 31);
int f = uf & 0x7fffff;
int e = (uf >> 23) & 0xFF;

如果为特殊值(无穷大和 NaN),那么保持不变返回即可。

如果为非规格数,uf << 1 即可得到符号位之外的正确结果(如果尾数部分溢出了会刚好溢出到阶码中),接上符号位即可得到答案。

如果为规格数,那么将阶码 \(+1\),如果阶码变成了 0xFF,答案就是无穷大,否则将符号、新的阶码和尾数拼接即可。

Code

unsigned floatScale2(unsigned uf) {
  int s = uf & (1 << 31);
  int f = uf & 0x7fffff;
  int e = (uf >> 23) & 0xFF;
  if (e == 0xFF) return uf;
  if (e == 0) return (uf << 1) | s;
  ++e;
  if (e == 0xFF) return s | 0x7f800000;
  return s | (e << 23) | f;
}

\(14\) 次操作。

floatFloat2Int(uf)

题意

任务:以 unsigned 类型给出一个 \(32\) 位浮点数 \(uf\) 的位级表示,求出 (int)uf 的结果。

允许使用的运算符:所有整数操作符,包括 &&, ||if, while

操作数限制:\(30\)

Solution

将阶码 \(- 127\) 得到真正的指数 e

如果 \(e < 0\),那么说明 \(|uf| < 1\),答案是 \(0\)

如果 \(e \geq 31\),那么说明 \(|uf| \geq 2^{31}\),超过了 int 的表示范围,返回题目要求的 0x80000000u。(ps:不用特判 \(uf = -2^{31}\) 的情况,因为 0x80000000u 刚好就是对应的值)

否则,我们取出尾数,再在最前面加上一个 \(1\)(浮点表示法中被省略的 \(1\)),就是尾数表示的值乘上 \(2^{23}\)。根据浮点数的结构,依据 \(e\)\(23\) 的大小将尾数左右移即可。如果 \(e \geq 23\),那么将尾数左移 \(e - 23\) 位,否则右移 \(23 - e\) 位。

Code

int floatFloat2Int(unsigned uf) {
  int s = uf & (1 << 31);
  int f = (uf & 0x7fffff) | 0x800000;
  int e = ((uf >> 23) & 0xFF) - 127;
  if (e < 0) return 0;
  if (e >= 31) return 0x80000000u;
  if (e >= 23) f <<= e - 23;
  else f >>= 23 - e;
  if (s) f = -f;
  return f;
}

\(15\) 次操作。

floatPower2(x)

题意

任务:\(2^x\) 的浮点数位级表示。

允许使用的运算符:所有整数操作符,包括 &&, ||if, while

操作数限制:\(30\)

Solution

浮点数阶码的表示范围是 \(-126\)\(127\)

如果 \(x > 127\),那么答案就是无穷大。

否则,如果 \(x \geq -126\),那么可以用规格数表示,将 \(x + 127\) 嵌进 \(23\)\(30\) 位上即可((x + 127) << 23)。

否则,如果 \(x \geq -126 - 23 = -149\),那么 \(x\) 可以用非规格数表示,因为非规格数尾数部分第 \(i\) 位表示 \(2^{i - 23} \times 2^{-126}\),因此答案就是 1 << (x + 149)

否则无法表示,返回 \(0\)

Code

unsigned floatPower2(int x) {
  if (x > 127) return 0x7f800000;
  if (x >= -126) return (x + 127) << 23;
  if (x >= -149) return 1 << (x + 149);
  return 0;
}

\(9\) 次操作。

ps:这个问题测试数据好像特别多,在时间限制内评测不完,需要在 btest.c 中将 TIMEOUT_LIMIT 宏改大一些,比如 \(20\)

posted @ 2024-04-24 13:12  hankeke303  阅读(12)  评论(0编辑  收藏  举报