CSAPP 之 DataLab 詳解

之一Yo發表於2022-05-07

前言

本篇部落格將會剖析 CSAPP - DataLab 各個習題的解題過程,加深對 int、unsigned、float 這幾種資料型別的計算機表示方式的理解。

DataLab 中包含下表所示的 12 個習題,其中 9 個和整數有關,3個和單精度浮點數有關。

函式名 功能描述 分數 操作符
bitXor(x, y) 使用 & 和 ~ 實現異或操作 1 14
tmin() 補碼的最小值 1 14
isTmax(x) x 是否為補碼的最大值 1 10
allOddBits(x) x 的奇數位是否全為 1 2 12
negate(x) 不使用 - 計算 x 的相反數 2 5
isAsciDigit(x) x 是否在 [0x30, 0x39] 區間內 3 15
conditional 實現條件運算子,x ? y : z 3 16
isLessOrEqual(x, y) x 是否小於等於 y 3 24
logicalNeg(x) 不使用 ! 計算邏輯非 4 12
howManyBits(x) 表示 x 的最少補碼位數 4 90
floatScale2(uf) 計算無符號數 uf 所表示的浮點數的 2 倍值 4 30
floatFloat2Int(uf) 將無符號數 uf 所表示的浮點數轉為整數 4 30
floatPower2(x) 計算 \(2^x\) 4 30

解題

整數題目

整數題目對程式碼的要求比較嚴格,不允許使用超過 0xFF 的整數字面量,也不能使用 if、while 等關鍵字,只能使用最基本的加法和位操作實現所需功能。

bitXor(x, y)

題目要求只使用 ~ 和 & 實現異或,我們只需用德摩根定律對異或的布林表示式做一下變換即可:

\[x\oplus y = \bar{x}y+x\bar{y} = \overline{\overline{\bar{x}y} \cdot \overline{x\bar{y}} } \]

有了上面的式子之後就很簡單了,程式碼如下:

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

tmin()

對於 4 個位元組的有符號數,\(T_{min}=-2^{32-1}=0b10\cdots0\),只需將 1 左移 31 位即可得到。

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

isTmax(x)

對於 4 個位元組的有符號數,\(T_{max}=2^{32-1}-1=0b01\cdots1\),題目不允許使用移位操作,所以有必要利用一下 \(T_{max}\) 的性質來解題:

\[T_{max}=0b01\cdots1=\sim 0b10\dots0=\sim T_{min}\\ -T_{min}=\sim T_{min}+1=T_{min} \]

也就是說,如果 x 是 \(T_{max}\) ,只需對它按位取反,再判斷它滿不滿足相反數即自身這個性質即可。但是除了 \(T_{min}\) 之外,0(\(\sim-1=\sim0b1\cdots1=0\)) 也滿足相反數即自身這一特點,所以需要將其排除。程式碼如下:

/*
 * 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) {
    int y = ~x;
    int y_ = ~y + 1;
    int isZero = !(y ^ 0);
    return !isZero & !(y ^ y_);
}

allOddBits(x)

對於所有奇數位都是 1 的整數,一定滿足下式:

\[x = 0b1x_{30}1x_{28}\cdots1x_{0}, 其中 x_{2i}\in \{0, 1\}\\ x\ |\ (x \gg 1)=0b11\cdots 1 \]

將 x 按位或右移 1 位的 x 一定可以得到每位都是 1 的整數,也就是 -1。但是有一個例外,當 x 為 0b1101(這裡自取 4 位,方便理解)時,雖然他沒有滿足所有奇數位都是 1 的要求,但是仍然有 \(x\ |\ x\gg1=1101 | 1110=1111\),所以我們有必要將 x 中的 4 的整數倍位清 0,即 \(x_{4i}=0\),由於這些都是偶數位,所以不必有任何的顧慮。

只需將 \(x\& 0xEEEEEEEE\) 就能做到上述的清零操作,整數實驗不允許使用大於 255 即 0xFF 的字面量,所以我們只能通過移位來構造 0xEEEEEEEE,程式碼如下:

/*
 * 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) {
    int mask = 0xEE + (0xEE << 8);
    mask = mask + (mask << 16);
    int y = x & mask;
    int z = y | (y >> 1);
    return !(~z ^ 0);
}

negate(x)

要計算相反數,只需按位取反之後再加 1 即可。

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

isAsciiDigit(x)

這題要判斷 x 是否為 Ascii 碼 0~9 中的某一個,即要求 \(0x30\le x \le 0x39\),可以分兩步實現判斷。

首先判斷低 4 位 \(x_3x_2x_1x_0\) 是否在 0~9 範圍內。當 \(x_3\) 為 0 時,低 4 位在 0~7 範圍內;當 \(x_3\) 為 1 時,只要 \(x_2\)\(x_1\) 為 0,低四位就在 8~9 範圍內。由此得到的布林表示式為:

\[A=\bar{x}_3+x_3\bar{x}_2 \bar{x}_1 \]

接著判斷 \(x_7x_6x_5x_4\) 是否為 3,只要將 x 右移 4 位之後異或 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) {
    // 判斷低 4 位是否在 0~9 範圍內
    int is0To9 = !((x & 8) ^ 0) + !((x & 14) ^ 8);
    // 判斷高 4 位是否為 3
    int isThree = !((x >> 4) ^ 3);
    return is0To9 & isThree;
}

conditional(x, y)

要實現 w = x : y ? z,只需實現函式 \(f(x, y, z)=z\ \&\ g(x)+y\ \& \ \sim g(x)\),其中 \(g(x)\) 滿足下式:

\[g(x)=\left\{ \begin{aligned} 0b11\cdots 1 \quad & x=0 \\ 0b00\cdots 0 \quad & x\neq 0 \end{aligned} \right. \]

要實現 \(g(x)\),只需先將 x 異或 0,如果 x 為 0,結果就是 0,否則為非 0 數,接著再邏輯取反,得到的數不是 1 就是 0,再按位取反並加 1,就能得到 \(g(x)\)。程式碼如下:

/*
 * 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) {
    int mask = ~(!(x ^ 0)) + 1;
    return (y & ~mask) + (z & mask);
}

isLessOrEqual(x, y)

比較兩個數的大小,首先應該比較符號位。如果 x 為正,y 為負,直接返回 0;如果如果 x 為負,y 為正,直接返回 1。

如果 x 和 y 同號,則判斷 \(z=x-y\le0\) 是否成立。由於題目不允許使用減號操作符,所以換成判斷 \(z=x+(-y)=x+(\sim y+1)\le 0\)。只要 z 的符號位為 1,x 就小於 y,如果 z 為 0,說明 x 等於 y。

/*
 * 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 signX = (x >> 31) & 1;
    int signY = (y >> 31) & 1;

    // 大小比較
    int z = x + (~y + 1);
    int isLe = !((z & (1 << 31)) ^ (1 << 31)) | !(z ^ 0);

    return (!(~signX & signY)) & ((signX & ~signY) | isLe);
}

logicalNeg(x)

邏輯取反,x 非 0 返回 0,x 為 0 返回 1。在實現 isTmax(x) 時,我們說過 0 滿足 \(-0=0\),即 \(0\ |\ (\sim0+1)\) 得到的結果還是 0。而其他非 0 數按位或自己的相反數,符號位一定會是 1。由此可以寫出邏輯非的程式碼:

/*
 * 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) {
    return ((x | (~x + 1)) >> 31) + 1;
}

howManyBits(x)

題目要求計算出表示 x 的最少補碼位數,比如:

  • \(0=0b0\),只需 1 位即可表示
  • \(-1=0b1\),也只需 1 位來表示
  • \(1 = 0b01 \in[-2, 1]\),需要 2 位來表示
  • \(-2=0b10\in [-2, 1]\),需要 2 位來表示
  • \(2=0b010\in [-4, 3]\),需要 3 位來表示
  • \(3=0b011\in [-4, 3]\),需要 3 位來表示
  • \(-3=0b101\in[-4, 3]\),需要 3 位來表示

觀察上面的二進位制數和他們所需的位數,可以發現如果 x 為正數,從左到右掃描,第一個 1 出現的位置 +1 就是所需位數。如果 x 為負數,將其按位取反轉換為正數後再進行相同判斷即可。

我們可以採用二分法來從左到右尋找第一個 1 出現的位置。首先去高 16 位看看有沒有 1 出現,如果有就把 x 右移 16 位後的值賦給 x,再去移位後 x 的低 16 位二分查詢。如果高 16 位沒有出現 1,就在低 16 位二分查詢。

int howManyBits(int x) {
    int sign = x >> 31;

    // 將 x 轉換為正數,這樣只要判斷最高位 1 出現的位置即可
    x = (sign & ~x) | (~sign & x);

    // 判斷高16位是否存在 1,如果有就右移 x
    int b16 = (!!(x >> 16)) << 4;
    x = x >> b16;

    // 判斷高 8 位是否存在 1,如果有就右移 x
    int b8 = (!!(x >> 8)) << 3;
    x = x >> b8;

    // 判斷高 4 位是否存在 1,如果有就右移 x
    int b4 = (!!(x >> 4)) << 2;
    x = x >> b4;

    // 判斷高 2 位是否存在 1,如果有就右移 x
    int b2 = (!!(x >> 2)) << 1;
    x = x >> b2;

    int b1 = !!(x >> 1);
    int b0 = x >> b1;

    return b16 + b8 + b4 + b2 + b1 + b0 + 1;
}

浮點數題目

浮點數題目對程式碼的要求沒有整數題目那麼嚴格,可以在程式碼裡面使用超過 0xFF 的整數字面量,可以使用 if、while 關鍵詞,還能使用 ==、>= 等邏輯運算子。

floatScale2(uf)

題目要求將無符號數 uf 表示的單精度浮點數 f 乘以 2,可以分為兩種情況:

  • 如果 f 為非規格化數,即 exp 欄位為 0,此時 f 小於 1,只需將 uf 算術左移即可
  • 如果 f 為規格化數,即 exp 欄位不為 0,乘以 2 只需將 exp+1 即可,但是 +1 之後可能使得 exp 變為 0xFF,即發生了溢位,這時候需要返回 \(+\infty\) 或者 \(-\infty\)

程式碼如下所示:

/*
 * 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 exp = (uf & 0x7f800000) >> 23;
    if (exp == 255) {
        return uf;
    }

    // 取出符號位
    unsigned sign = uf & 0x80000000;

    // 非規格化數,直接左移擴大兩倍
    if (exp == 0) {
        return uf << 1 | sign;
    }

    // 溢位
    if (++exp == 255) {
        return sign | 0x7f800000;
    }

    return exp << 23 | (uf & 0x807fffff);
}

floatFloat2Int(uf)

題目要求將浮點數 f 強轉為整數,根據 \(E=exp-Bias\) 的值可以分為幾種情況:

  • 如果 \(E\) 小於 0,說明 f 要麼是非規格化數(\(exp\) 為 0,這裡沒有使用 \(1-Bias\) 因為只看 \(E\) 的符號),要麼是一個小於 2 的數乘上了 \(1/2^n\) ,兩種情況下 f 的絕對值都小於 1,只需返回 0 即可
  • 如果 \(E\) 大於 31,說明 \(|\pm 1.XX\cdots X|\) 至少變成原來的 \(2^{32}\) 倍,由於整數只有 4 個位元組,這時候發生了溢位,返回 0x80000000
  • 如果 \(23\lt E\lt 31\),說明 \(|\pm 1XX\cdots X|\) (注意這裡沒有小數點,所以需要大於 23)需要左移(擴大)才能表示浮點數的值 ,左移的過程中可能改變符號為負,說明發生了溢位,需要返回 0x80000000
  • 如果 \(0\le E\le 23\),說明 \(|\pm 1XX\cdots X|\) 需要右移(縮小)才能表示浮點數的值

程式碼如下所示:

/*
 * floatFloat2Int - Return bit-level equivalent of expression (int) f
 *   for floating point argument f.
 *   Argument is passed as unsigned int, but
 *   it is to be interpreted as the bit-level representation of a
 *   single-precision floating point value.
 *   Anything out of range (including NaN and infinity) should return
 *   0x80000000u.
 *   Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
 *   Max ops: 30
 *   Rating: 4
 */
int floatFloat2Int(unsigned uf) {
    // 計算階碼
    unsigned exp = (uf & 0x7f800000) >> 23;
    int e = exp - 127;

    // 0或小數直接返回 0
    if (e < 0) {
        return 0;
    }

    // NaN 或者 無窮大
    if (e > 31) {
        return 0x80000000;
    }

    // 尾數
    int frac = (uf & 0x7fffff) | 0x800000;

    // 移動小數點
    if (e > 23) {
        frac <<= (e - 23);
    } else {
        frac >>= (23 - e);
    }

    // 符號位不變
    if (!((uf >> 31) ^ (frac >> 31))) {
        return frac;
    }

    // 符號位變化,且當前符號為負,說明溢位
    if (frac >> 31) {
        return 0x80000000;
    }

    // 符號變化,返回補碼
    return ~frac + 1;
}

floatPower2(x)

這題比較簡單,要求計算 \(2^x\) ,只要將 exp 加上 x 即可。因為 x 變化範圍太大,可能導致 exp 小於 0 或者大於 255,這時候就要返回 0 或者無窮大。

/*
 * 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) {
    int exp = 127 + x;

    // 溢位
    if (exp >= 255) {
        return 0x7f800000u;
    }

    // 太小以至於無法用非規格化數來表示
    if (exp < 0) {
        return 0;
    }

    return exp << 23;
}

總結

做完習題之後收穫還是挺大的,做題的過程也產生了一些想法:

  • 看書還是挺無聊的,配合 B 站的網課食用更香,而且看 CMU 網課的感覺和看國內慕課的感覺完全不一樣,看慕課的時候只想著開倍數刷完了事,而看 CMU 網課的時候就覺得大牛慢慢悠悠的節奏很舒服,可以看得很投入
  • int 和 unsigned 的底層二進位制數是一樣的,只是看待這個二進位制數的方式不同,只要記住數軸即可

以上~~

相關文章