深入理解计算机系统-datalab详解

下载实验用的文件戳这里

bitXor

(以下推导方法改编自 StackOverflow 用户 @Mike 的回答。比心~)
这道题呢,我们可以列一张表:

x x x y y y x x x ^ y y y
000
011
101
110

我们要怎么做才能找到这三者之间的关系呢?既然题中要求了要用 & 或者 ~ 来实现,我们就给 “&” 也列一张表:

x x x y y y x x x & y y y
000
010
100
111

我相信,你一定在离散数学或者数字电路这门课里接触过一种运算叫”或非“。那么我们给 “或非”(NOR) 也列一张表:

x x x y y y x x x NOR y y y
001
010
100
110

之后我们把这三张表放在一起:

x x x & y y y x x x NOR y y y x x x ^ y y y
010
001
001
100

观察到了吗?前两列做或非运算是不是就能得到第三列?所以我们可以把异或运算写成这种形式:
x    X O R    y = ( x    A N D    y )    N O R    ( x    N O R    y ) x\; \mathrm{XOR} \;y = (x \;\mathrm{AND} \;y)\; \mathrm{NOR} \;(x \;\mathrm{NOR} \;y) xXORy=(xANDy)NOR(xNORy)
想一想,或非运算可以怎样用 & 和 ~ 两个运算符表示呢?
这个不难。很显然,

x    N O R    y = \qquad\qquad\qquad x\; \mathrm{NOR} \; y = xNORy= ~ x x x & ~ y y y
把这个表示代入上式,就能得到下面的C语言表达式:

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

成功啦。

tmin

很常规的题啦~ 考的是定义。

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

isTmax

这道题要我们比较一个数是不是和int可以表示的最大值相等。当然最自然的办法是先凑出 0x7fffffff,之后和 x x x 比较。可是很遗憾,这道题不允许我们使用左移右移操作,因此 0x7fffffff 是不容易凑出的。
那应该怎么办呢?
我们知道,0x7fffffff 取反之后的结果 0x80000000 有一个特点:取它的补码,结果仍为 0x80000000. 所以我们借助这个特点来写这个函数。注意, 0 取补码的结果也依然为 0. 把为 0 的情况排除掉就可以了。

int isTmax(int x) {
  int negate1 = ~x; // 取反
  int negate2 = ~negate1 + 1; // 取取反结果的补码
  return (!(negate2 ^ negate1)) & !!negate1;
  //     取过补码之后,值有变化吗?  negate1 为零吗?
}

allOddBits

按照题中的要求,只要参数 x x x 的所有奇数位都为一,就可以返回 1,而对它的偶数位没有具体要求。既然没有要求,那我们的发挥空间就很大了——直接把 x x x 的偶数位全变成一,之后把 x x x 按位取反,再和零比较是否相等不就可以了吗?所以这个函数可以这样写:

int allOddBits(int x) {
  int combination = x | 
  			(0x55 | (0x55 << 8) | (0x55 << 16) | (0x55 << 24));
  // 拼出0x55555555,再和x按位或
  int ans = !(~combination ^ 0);
  return ans;
}

negate

基础的基础的基础!!!

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

isAsciiDigit

这道题的本质是让我们判断 x x x 是不是在0x30和0x39之间。用位运算怎么比较大小呢?两数作差,取符号位就可以了。

int isAsciiDigit(int x) {
  int isAbove0x30 = !(((x + ((~0x30) + 1)) >> 31) & 1);
  int isBelow0x39 = !(((0x39 + (~x + 1)) >> 31) & 1);
  return isAbove0x30 & isBelow0x39;
}

logicalNeg

如果一个数 x x x非零,那么它的相反数 − x -x x也一定不为零,对吧?
那么, x x x − x -x x, 一定有一个的符号位为一,对吧?所以,只要我们取 x x x的相反数,把它和原来的 x x x拼在一起,再取符号位,是不是就可以把零和其他数区分出来了?区分出来之后呢,我们只要把刚才那个拼在一起的结果右移31位,是不是就得到-1(或者是零)了?再加上一,不就达到要求了嘛。

int logicalNeg(int x) {
  int combination = x | (~x + 1);
  return (combination >> 31) + 1;
}

conditional

这道题让我们模拟三目运算符——如果参数 x x x 为不零,就返回参数 y y y ; 如果为零,就返回参数 z z z . 那么如何控制返回哪个值呢?我们可以先做一个类似掩码的东西——这个数的每一位,不是全为零,就是全为一,根据 x x x 的取值而定。做这个掩码的一种方法是借助左移和右移,就像下面这样:

int mask = ((!!x) << 31) >> 31;

另外一种方法是凑-1:

int mask = ~(!!x) + 1;

结果是一样的——如果 x x x 不是零,mask就是 0xffffffff,反之就是 0。
这个mask怎么用?借助取反和按位或就可以:

int ans = (~mask & z) | (mask & y);

所以最终的代码是这样的:

int conditional(int x, int y, int z) {
  int mask = ((!!x) << 31) >> 31;
  int ans = (~mask & z) | (mask & y);
  return ans;
}

isLessThanOrEqual

起初你可能会觉得,这道题像上面那样作差就ok:

int isLessOrEqual(int x, int y) {
  int difference = y + (~x + 1);
  int sign = (difference >> 31) & 1;
  return !sign;
}

但是!但是!但是!你会发现这个当 y y y x x x 只要有一个是int可以表示的最大值或者最小值的时候,函数就无法返回正确的值!
为什么呢?首先,如果 x x x 是0x80000000,那么这个数取相反数用int是表示不出来的!这样不管 y y y 是多少,difference的符号位总是1!其次,如果 x x x y y y 都是很接近int极限的数,并且一正一负,那么算difference时几乎一定会溢出!这时符号位是多少是没有意义的!
因此,正确的做法是,除了要判断difference的符号,也要对比 x x x y y y 的符号:如果 y y y 为正, x x x 为负,就不需要多余的比较了。

int isLessOrEqual(int x, int y) {
  int difference = (unsigned)y + (unsigned)(~x + 1);
  int sign_x = (x >> 31) & 1;
  int sign_y = (y >> 31) & 1;
  // 取x、y的符号
  int is_xy_share_same_sign = !(sign_x ^ sign_y);
  // x、y符号是否相同呢?
  int sign = (difference >> 31) & 1;
  return (!sign & is_xy_share_same_sign) | 
  // x、y符号相同时才根据difference的符号判断大小
    (!is_xy_share_same_sign & (((sign_y + (~sign_x + 1)) >> 31) & 1));
    // x、y符号不相同就直接看是不是y为正、x为负
}

howManyBits

看到这个函数的要求,不知道你有什么想法?
如果一下子没有思路的话,也没有关系,我们先回忆一下有关补码的基础知识:
我们设n位位向量 ω ⃗ = ( ω n − 1 , ω n − 2 , … , ω 2 , ω 1 , ω 0 ) \vec{\omega} = (\omega_{n-1}, \omega_{n-2}, \ldots ,\omega_{2}, \omega_{1}, \omega_{0}) ω =(ωn1,ωn2,,ω2,ω1,ω0). 其中对于任意的 k ∈ { 0 , 1 , … , n − 1 } k \in \{0, 1, \ldots, n-1\} k{0,1,,n1} ,有 ω k ∈ { 0 , 1 } \omega_k \in \{0, 1\} ωk{0,1}. 想想看,按照补码的运算方式, ω ⃗ \vec{\omega} ω 这个位向量可以表示出的最大数是什么呢?没错,就是当 ω n − 1 = 0 \omega_{n-1} = 0 ωn1=0, 其余的 ω k \omega_k ωk全部等于一时,下面这个和式:
Ω max ⁡ = 2 n − 2 + 2 n − 3 + … + 2 1 + 2 0 \Omega_{\max} = 2^{n-2} + 2^{n-3} + \ldots + 2^{1} + 2^{0} Ωmax=2n2+2n3++21+20.
就是等比数列求和嘛!所以最终我们有:
Ω max ⁡ = 1 × ( 1 − 2 n − 1 ) 1 − 2 = 2 n − 1 − 1. \Omega_{\max} = \frac{1\times(1-2^{n-1})}{1-2} = 2^{n-1}-1. Ωmax=121×(12n1)=2n11.
同样,我们可以知道,这个位向量可以表示出的最小的数是 Ω min ⁡ = − 2 n − 1 \Omega_{\min} = -2^{n-1} Ωmin=2n1.
好啦,回忆结束。现在再来想一想,这些基础知识和这个函数要算的东西之间,有什么样的联系呢?
相信你一定想到了——解方程就ok. 如果我们在 x x x 为非负数时,令 Ω max ⁡ = x \Omega_{\max} = x Ωmax=x; 在 x x x 为负数时,令 Ω min ⁡ = x \Omega_{\min} = x Ωmin=x, 根据上面的等式解出 n n n 的值,再向上取整,不就得到 howManyBits 要求返回的值了吗?如果用数学语言表达的话,howManyBits 就是下面这个东西:
h o w M a n y B i t s ( x ) = { ⌈ log ⁡ 2 ( x + 1 ) ⌉ + 1 , x ≥ 0 ⌈ log ⁡ 2 ( − x ) ⌉ + 1 , x < 0 howManyBits(x)= \left\{ \begin{array}{ll} \lceil\log_2 (x+1)\rceil+1, x\ge0\\ \lceil\log_2(-x)\rceil + 1, x<0 \end{array} \right. howManyBits(x)={log2(x+1)+1,x0log2(x)+1,x<0
然而怎么用位运算实现呢?
像用位运算取以二为底的对数之类看着就基础的操作,一定有前人实现好了。所以我们直接去 StackOverflow 上搜索。果不其然,我们搜到了相似的问题,往下翻就可以看到下面这段代码(来自 @phuclv 的回答):

unsigned int v;	         // 32-bit value to find the log2 of 
register unsigned int r; // result of log2(v) will go here
register unsigned int shift;

r =     (v > 0xFFFF) << 4; v >>= r;
shift = (v > 0xFF  ) << 3; v >>= shift; r |= shift;
shift = (v > 0xF   ) << 2; v >>= shift; r |= shift;
shift = (v > 0x3   ) << 1; v >>= shift; r |= shift;
                                        r |= (v >> 1);

(事实上,这段代码的最初来源是这里。根据网站中的说明,csapp 的作者之一 Bryant 验证过其中展示代码的正确性。)
其实我们还可以给它重新排下版,加上注释:

unsigned int log2(unsigned int v)
{
    unsigned int r; // result of log2(v) will go here
    unsigned int shift;
    r = (v > 0xffff) << 4; 
    // v大于0xffff吗?如果是,就把r的第五位设成1.
    // 0x10000(65536)取以二为底的对数,结果是0x10(0b10000). 
    // 0xffffffff取以二为底的对数,结果是0x1f(去尾)(0b11111).
    // 因为v是unsigned int,所以r从第六位开始,之后的任何一位都不可能出现一。
    // 大于号不能用?不要紧,把括号里的内容改成(!!(v >> 16))就可以了。
    // 下面的改法与这里相似。
    v >>= r; 
    // 大于2^r的部分检查完了。给v除上2^r, 检查余下的部分。

    shift = (v > 0xff) << 3; 
    // v大于0xff吗?如果是,就把r的第四位设成1.
    // 为什么是0xff?原因和上面类似——只要v/2^r的值介于0xff和0xffff之间,
    // 结果的第四位就一定是一。
    v >>= shift; 
    r |= shift;

    shift = (v > 0x7) << 2; 
    v >>= shift; 
    r |= shift;

    shift = (v > 0x3) << 1; 
    v >>= shift; 
    r |= shift;

    r |= (v >> 1);
    // 到了这一步,v的取值只可能是0、1、2.
    // 如果v等于2呢,就把r的第一位设成1.
    return r;
    // 算完就返回啦~不过返回的结果是去尾的。
}

虽然 log ⁡ 2 x \log_2x log2x 的图像是条曲线,但是 ⌊ log ⁡ 2 x ⌋ \lfloor \log_2x\rfloor log2x 可就不是了~ 因此这个函数的基本原理就是找到参数 v v v 对应的范围,之后返回对应的值。更详细的注释都在上面的代码里了~
但是这个 lab 的要求是不能用除了 int 之外的数据类型……怎么办呢?如果我们仅仅是简单地将 unsigned int 改成 int,将会导致 v v v 的第 31 位为一时, r r r 中的结果不正确——随着前面我们不停的对 v v v 算术右移, v v v 的高位会被 1 填满。那么,在最后一步计算 r | (v >> 1) 时, r r r 的高位自然也会被跟着填满。解决方案很简单——在这一步只取 v v v 的最低一位就可以了。
那么向上取整怎么做呢?我们知道,上面 log2 函数返回的值,如果是被取过整的话, 2 r 2^r 2r 肯定不会还等于原来的 v v v 。这样看,向上取整的操作就很简单了——算 2 r 2^r 2r 的值,如果不等于原始的 v v v 值,就说明需要向上取整。至于 x x x 大于等于零和小于零时代入的值不同的问题,我们直接套用前面 conditional 函数里用过的逻辑。
所以,最后我们的函数是这样实现的:

int howManyBits(int x) {
  int mask = x >> 31;
  int value = (mask & (~(x + ~0))) | (~mask & (x + 1));
  // 这是 conditional 里用过的逻辑
  int processed_x = value;

  // 下面几行是取对数的代码
  int r; // result of log2(value) will go here
  int shift;
  r = (!!(value >> 16)) << 4; value >>= r;
  shift = (!!(value >> 8)) << 3; value >>= shift; r |= shift;
  shift = (!!(value >> 4)) << 2; value >>= shift; r |= shift;
  shift = (!!(value >> 2)) << 1; value >>= shift; r |= shift;
  r |= ((value >> 1) & 1);
  // 最后只取 value 的低位

  r += !!(processed_x ^ (1 << r));
  // 需要向上取整吗?如果需要,就把r的值加上一。
  r += 1;
  return r;
}

这样就可以啦。当然网上其他题解中的办法比上面的办法更直接,不过本质都是取以二为底的对数。

floatScale2

加油加油加油!前面整数部分的题都已经做完啦~ 下面的浮点部分,因为可以用 if 语句处理输入,相对就容易多了~
要把规格化的浮点数乘二,只要把阶码加一,再把加一之后的值拼回去。
对于非规格化的浮点值,我们直接把它的小数部分左移一位,再拼回去。

你可能会担心:如果左移小数部分,导致小数部分高位的一溢出到了阶码的低位,还能不能得到正确的结果呢?其实这时的结果也是正确的。假如我们设移动之前的 uf 是下面这个位向量:
u f ⃗ = ( s , 0 , … , 0 ⏟ 8 个 0 , f 22 , f 21 , … , f 0 ) \vec{uf}=(s, \underbrace{0, \ldots, 0}_{8个0}, f_{22}, f_{21}, \ldots, f_0) uf =(s,80 0,,0,f22,f21,,f0)
这时,
E = − 126 E=-126 E=126
M = f 22 ⋅ 2 − 1 + f 21 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 23 M=f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23} M=f2221+f2122++f0223
于是 u f ⃗ \vec{uf} uf 表示的值就是
v = ( − 1 ) s × 2 − 126 × ( f 22 ⋅ 2 − 1 + f 21 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 23 ) v = (-1)^s \times 2^{-126} \times (f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}) v=(1)s×2126×(f2221+f2122++f0223)
那么,左移一位之后是什么情况呢?
这时, E E E 依然是 − 126 -126 126(对应溢出的情况), 但别忘了,此时 uf 已经是规格化的浮点数了,所以 M M M 表示的值是这个:
M = 1 + f 21 ⋅ 2 − 1 + f 20 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 22 M= 1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22} M=1+f2121+f2022++f0222
此时 u f ⃗ \vec{uf} uf 表示的值是
v ′ = ( − 1 ) s × 2 − 126 × ( 1 + f 21 ⋅ 2 − 1 + f 20 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 22 ) v' = (-1)^s \times 2^{-126} \times (1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}) v=(1)s×2126×(1+f2121+f2022++f0222)
我们给这两个数作比:
v ′ v =   1 + f 21 ⋅ 2 − 1 + f 20 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 22 f 22 ⋅ 2 − 1 + f 21 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 23 =   1 + f 21 ⋅ 2 − 1 + f 20 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 22 2 − 1 + f 21 ⋅ 2 − 2 + … + f 0 ⋅ 2 − 23 =   2 \Large \begin{array}{ll} \frac{v'}{v}&=\ \frac{1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}}{f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}}\\\\ &=\ \frac{1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}}{2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}}\\\\ &=\ \normalsize2 \end{array} vv= f2221+f2122++f02231+f2121+f2022++f0222= 21+f2122++f02231+f2121+f2022++f0222= 2
神奇嘛~ 这就是说,根据神奇的 IEEE 754 标准,对于把非规格浮点值乘二的操作,我们根本无需担心小数部分溢出的情况——直接左移一位就可以了(不过如果乘的不是二,就不能直接左移了)。
所以,最终我们的代码是这样写的:

unsigned floatScale2(unsigned uf) {
  int exponent = (uf >> 23) & 0xff; // 取阶码
  int fraction = uf & (0xff | (0xff << 8) | (0x7f << 16)); 
  // 取小数部分
  int sign = uf & (1 << 31); // 取符号位
  if (exponent != 0xff) // 是无穷大或者 NaN 吗?
  {
    if (exponent == 0) // 非规格化的情况
    {
      fraction <<= 1; 
      return fraction | sign; 
      // 左移一位之后和符号位拼在一起
    }
    exponent += 1; // 阶码加一
    if (exponent == 0xff) 
      uf = sign;
    // 无穷大?清掉 uf 除了符号位之外的部分
    exponent <<= 23; 
    uf &= ~(0xff << 23); 
    // 先抹掉 uf 原来的阶码
    uf |= exponent;
    // 再把新阶码拼上
    return uf;
  }
  // 是无穷大或者 NaN 就直接返回 uf
  return uf;
}

floatFloat2Int

我们知道,对于参数 uf,它所对应的值是这样算出来的: V = ( − 1 ) s × M × 2 E V = (-1)^s \times M \times 2^E V=(1)s×M×2E
其中 M M M 是一个二进制小数。类比十进制,你觉得 M × 2 E M \times 2^E M×2E 会怎样让 M M M 的值变化呢?
我们先从十进制的情况入手。假设 x x x 是一个十进制小数(比如,3.1415926535)。之后,我们给 x x x 乘上 1 0 n 10^n 10n(也就是 ( 9 + 1 ) n (9 + 1)^n (9+1)n,对应前面的2)。你会发现小数点向后移动了 n n n 位对不对。那么放到二进制里,情况也是一样的。乘以 2 E 2^E 2E, 就相当于把 M M M 的小数点向后移动 E E E 位。这道题的要求是把浮点数转换成整数,因此我们只要确定小数点会被移动到哪个位置,之后取出小数点之前的所有位,就是 V V V 转换成整数之后的值。

int floatFloat2Int(unsigned uf) {
  int sign = uf >> 31; 
  int exponent = (uf >> 23) & 0xff;
  int E = exponent - 127; 
  int fraction = uf & (0xff | (0xff << 8) | (0x7f << 16)); 
  int offset = 23 - E; 
  // 算偏移量:应当把 fraction 右移多少位才能去掉小数点之后的部分?
  // offest 大于零,右移;小于零,左移
  if (E < 0) 
  // 非规格化的情况也包含其中了,因此 M 一定等于 1 + f
    return 0;
  if (exponent == 0xff || offset < -8)
    return (1 << 31);
  // 如果是非规格化的浮点值,或者无穷大,
  // 返回 0x80000000

  if (offset < 0)
    fraction <<= (-offset);
  if (offset >= 0)
    fraction >>= offset;

  fraction |= (1 << E); // 相当于 1 + f 中的 1.
  if (sign) // 如果原数是负的,就给 fraction 取相反数
    fraction = -fraction;
  return fraction;
}

floatPower2

这道题要求我们算 2. 0 x 2.0^x 2.0x. 我们按 x x x 的取值范围分成几种情况讨论:

  1. 0 ≤ x ≤ 128 \quad0 \leq x \leq 128 0x128
    这种情况对应的是中规中矩的规格化的情况。只要算出 exponent 是多少,再把它左移到合适的位置就可以了。
  2. x > 128 \quad x > 128 x>128
    这种情况下,exponent 算出来已经大于 255 了,用一个字节是放不下的。返回无穷大就好。
  3. − 126 ≤ x < 0 \quad-126 \leq x < 0 126x<0
    x x x 等于 -126 时,我们来到了规格化与非规格化的分界线——此时阶码为一,表示的值为 2 − 126 2^{-126} 2126. 和上面一样,也是算出 exponent 是多少,再左移到合适的位置就可以了。
  4. − 149 ≤ x < − 126 \quad-149 \leq x < -126 149x<126
    非规格化的值可不可以表示 2 x 2^x 2x 呢?答案是肯定的。不过这里的计算方法和上面有所不同。要表示 2 − 127 2^{-127} 2127,需要把返回值的第 22 位(小数部分的最高位)设成一,保持其他位为零。要表示 2 − 128 2^{-128} 2128 或者更小的数,只要在刚才的基础上把这个返回值右移对应的位数就可以了。
  5. x < − 149 \quad x < -149 x<149
    x x x 小于 − 149 -149 149 时就真的表示不出来了。这时返回零。

所以最终函数的实现是这样的:

unsigned floatPower2(int x) {
  if (x >= 0)
  {
    if (x <= 0x80)
    {
      int ans = (0x7f + x) << 23;
      return ans;
    }
    // 太大了
    return 0xff << 23;
  }
  if (x < 0)
  {
    // 规格化和非规格化的分界线: x = -126 时,阶码为 1.
    // 此时表示的值是 2 的 -126 次方。
    if (x >= -126) 
    {
      int ans = (0x7f + x) << 23;
      return ans;
    }
    // 非规格化的值应该怎么办?
    if (x >= -149)
    {
      int ans = 1 << (23 - (-x - 126));
      return ans;
    }
    // 再小就真的表示不出来了
    return 0;
  }
}

不过因为超时的问题这个函数过不了样例……测试时在 btest 后面加上 “-T 20” 把时间限制放宽一些就能通过了。
这样,csapp 的第一个实验 datalab 就做完了。给自己点个赞吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值