# Data lab 紀錄
contributed by < `justapig9020` >
###### tags: `CSAPP`
## Link
[data lab](https://github.com/justapig9020/csapp-labs/tree/main/data/datalab-handout)
## Lab 說明
Data lab 要求我們實做數個簡單的函數,然而在實做上包含諸多限制:
1. 只能宣告 int / long 型態的區域變數
2. 不能使用全域變數
3. 定義的常數必須在 [0, 0xff] 區間內
4. 不得轉型為 int / long 以外的型態
且以下的操作是完全禁止的:
1. 使用 if / while / for 等控制描述。
2. 定義或使用巨集( macro )
3. 定義額外的函數
4. 呼叫任何函數
5. 使用每個小題允許範圍外的運算子
6. 使用 int / long 以外的型態,包含 struct / enum / union 等。
## Lab 實做
### copyLSB
```c=
/*
* copyLSB - set all bits of result to least significant bit of x
* Examples:
* copyLSB(5L) = 0xFFFFFFFFFFFFFFFFL,
* copyLSB(6L) = 0x0000000000000000L
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
long copyLSB(long x) {
x <<= 63;
x >>= 63;
return x;
}
```
由於在 C 語言中,決定使用邏輯右移或是算術右移的條件為該型態是否為 signed 。因此,signed 的 long 在右移上會使用算術位移。
因此,我們首先只需要將 `x` 左移,使得原本的 `LSB` 變為 `MSB` ,再右移即可將其填滿整個變數。
### dividePower2
```c=
/*
* dividePower2 - Compute x/(2^n), for 0 <= n <= 62
* Round toward zero
* Examples: dividePower2(15L,1L) = 7L, dividePower2(-33L,4L) = -2L
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*/
long dividePower2(long x, long n) {
long is_negative = !!(x >> 63);
long round = (is_negative << n) + (~is_negative) + 1;
x += round;
return x >> n;
}
```
$\frac{x}{2 ^ n}$ 可以被為寫成為運算 $x >> n$ 。
然而題目另外要求必須向 0 做捨入,也就是當結果為鄭數時做無條件捨去,為負數時則為無條件進位。
此一要求在 x 為負數時就會出現問題,考慮負數時的算術右移,右移後小數點右方的數字會被無條件捨去,因此即便在負數時,算術右移的捨入依然為無條件捨去。
為了彌補損失,當 x 為負數時,我們需要在右移前先補償。規則為,當右移將被抹去的位數不全為零時,需要補償 +1 。因此在右移 n bit 前,我們先行加上 n bit 的 1 ,如此一來,唯有當低 n bits 皆為零時不會進位,反之會進位至第 n+1 bit ,使該位 +1 (n + 1 位為等等右移後的 LSB )。
回到計算本身,首先我們透過算術右移,提取 x 的 MSB ,若 x 為負數時, `is_nagative` 為 1 反之為 0 。
接著我們來製作捨入的素材 `round` 。
要製作 n bits 的連續的 1 方法為, 首先將 1 左移 n bits ,再將其 -1 。這邊會遇到的問題有 2:
1. 我們只需要在 `is_nagative` 為 1 時產生 round
2. 由於無法使用減號,因此需要自行生成 -1
由於第一條要求,我們只要在所有需要 1 的部份皆使用 `is_nagative` 代替,即可確保在 x 為正時所生成的 `round` 為 0 。
此外,我們可以透過 2 得補數的規則來生成 -1 。在 `is_nagative` 為 1 時透過補數產生 -1 來完成 `round` 。若 `is_nagative` 為 0 時,由於 -0 = 0 ,因此結果上 `round` 依舊為 0 。
最後我們只需要先將 `round` 與 x 相加,先完成捨入,再進行右移完成除法。
### distinctNegation
```c=
/*
* distinctNegation - returns 1 if x != -x.
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 5
* Rating: 2
*/
long distinctNegation(long x) {
long neg = ~x + 1;
return !!(x ^ neg);
}
```
頗為直覺的一題。
透過補數規則算出 -x , `neg` , 並透過 xor 檢測 x 與 neg 是否相等。因為,唯有兩數相等時 xor 的結果為 0 。最後在透過兩次 logic not 來將數值收斂至 0 / 1 。
### anyEvenBit
```c=
/*
* anyEvenBit - return 1 if any even-numbered bit in word set to 1
* where bits are numbered from 0 (least significant) to 63 (most significant)
* Examples anyEvenBit(0xAL) = 0L, anyEvenBit(0xEL) = 1L
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 14
* Rating: 2
*/
long anyEvenBit(long x) {
long mask = 0x55;
mask |= mask << 8;
mask |= mask << 16;
mask |= mask << 32;
return (long)(!!(x & mask));
}
```
依舊是直覺的一題。
透過建構一個在 even bits 為 1 的 `mask` 來檢測 x 在這些 bits 是否為 1 。只考慮單一 byte 的話, mask 的數值為 0b1010 = 0x55 ,因此完整的 `mask` 的數值為 0x5555555555555555 。
稍微需要花點心思的部份為 `mask` 的建制 ,由於規定不能定義大於 0xff 的數字,因此需要透過不斷的位移與或運算來建構 `mask` 。
### isLessOrEqual
```c=
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4L,5L) = 1L.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
long isLessOrEqual(long x, long y) {
// If x y have same sign, x ^ y's MSB is 0. 1 otherwise
long sign = !((x ^ y) >> 63);
// If x y have different sign and x is negative. Than x <= y always true
long diff = (x >> 63) & (!sign);
/*
* If x y have same sign, -x and y have different sign. Therefore, y - x
* never overflow. 0 <= y - x, if x is TMIN, -x is also TMIN. Since x, y
* have same sign. y is negative in this case, therefore y + x = y + TMIN
* always lead to a overflow and caused a positive number and always holds 0
* <= y - x
*/
long neg_x = (~x) + 1;
long same = (!((y + neg_x) >> 63)) & sign;
return same | diff;
}
```
若是從純數字上來解釋的話十分直覺。透過簡單的移位即可得到 $0 <= y - x$ ,然而這邊會遭遇到 overflow 的問題。由於 overflow 只會發生在 x, y 不同號的狀況 ( y - x = y + (-x)) ,因此可以將問題分為兩個部份考慮。
1. x, y 同號
2. x, y 不同號
透過 xor x, y 的 MSB 我們可以得出兩數是否同號。若 xor 的結果為 1 則兩者同號,反之則異號。
因此, `sign` 在兩者同號時為 1 。
若 x, y 不同號時,若透過 $0 \le y - x$ 的邏輯去判斷,可能會遭遇 overflow 的問題。然而針對此問題,其實我們只要判斷 x, y 的正負就可以得到答案。邏輯為:當x, y 異號,且 x 為負,則 $x \le y$ (因為 x, y 不同號,因此 y 必定為正)。
透過將 x 的 MSB 右移來判斷 x 的正負,再透過 `!sign` 確保 x, y 異號。
若 x, y 同號時,由於 $y-x$ 不具 overflow 的風險,因此可以透過 $0 \le y-x$ 的邏輯來判斷正負。
這邊需要考慮的問題為: x, y 都有可能為 TMIN ( 0x8000000000000000 ),由於 -TMIN = TMIN ,因此,假設 x 為 TMIN 則 $y-x = y+x$ 。反之亦然。
分開討論 x, y 為 TMIN 的狀況可以發現:
- 若 x 為 TMIN 則 $x \le y$ 永遠為真。
- 若 y 為 TMIN 則 $x \le y$ 在多試情況為假,但當 x 也為 TMIN 時,該命題為真。
基於此討論,發現我應該傾向移動 x ,因為當其為 TMIN 時的狀況只有一種,我們不須額外多討論其他狀況。
思考當 x 為 TMIN 時的狀況,因為 x, y 同號,因此此時 y 必為負,由於在此狀況下 $y - x = y + x = y + TMIN$ ,因此結果為一負數加上 TMIN 。此運算必定會產生 overflow 並產生一個正數,結果剛好符合 $0 \le y - x$ 因此為真,又恰好與 x 為 TMIN 狀態相同。結論,當我們移動 x 時,無須額外考慮 x 為 TMIN 的狀況。
運算上,我們首先透過補數運算出 -x , `neg_x` , 再透過判斷 y + neg_x 的 MSB 來判斷結果是否滿足 $0 \le y - x$ 。最後也要透過 `sign` 確保兩者同號。
最後,只須將同號與異號的結果進行或運算,即可得出結果。
### replaceByte
```c=
/*
* replaceByte(x,n,c) - Replace byte n in x with c
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: replaceByte(0x12345678,1,0xab) = 0x1234ab78
* You can assume 0 <= n <= 7 and 0 <= c <= 255
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 10
* Rating: 3
*/
long replaceByte(long x, long n, long c) {
long shift = n << 3; // n * 8
x &= ~(0xFFL << shift);
x |= c << shift;
return x;
}
```
在嵌入式領域常見的操作,需要特別注意的是 n 的單位是 byte ,然而位移操作的單位是 bit ,因此在位移前,要先將 n << 3 (n * 8) 來完成單位的轉換。
接著,首先透過位移 0xff 的 mask 來將指定的 byte 清 0 ,最後再透過或運算將指定的 byte 填上對應的數值即可。
### conditional
```c=
/*
* conditional - same as x ? y : z
* Example: conditional(2,4L,5L) = 4L
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
long conditional(long x, long y, long z) {
long condition = !!x;
long mask = (condition << 63) >> 63;
return (y & mask) | (z & ~mask);
}
```
思路為:可以透過 0xffffffffffffffff 的 mask 與 y, z 做及運算來達成選擇的效果。因此只要在 x 不為零時造出 0xffffffffffffffff 的 mask ,反之則使 mask 為 0x0000000000000000 即可。
首先透過兩次 logic not 將 x 收斂至 0 / 1 (實際上一次即可),並透過位移將 0 / 1 擴展至每一 bit 即可造出上述的 mask 。最後只須將 mask / ~mask 分別與 y, z 運算最後在將兩者或運算即可。
### bitMask
```c=
/*
* bitMask - Generate a mask consisting of all 1's
* between lowbit and highbit
* Examples: bitMask(5L,3L) = 0x38L
* Assume 0 <= lowbit < 64, and 0 <= highbit < 64
* If lowbit > highbit, then mask should be all 0's
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
long bitMask(long highbit, long lowbit) {
long neg1 = ~1L + 1;
long hi_mask = (((1L << highbit) + neg1) << 1) + 1;
long lo_mask = (1L << lowbit) + neg1;
return hi_mask & ~lo_mask;
}
```
我們需要製造兩個 mask ,一是從 highbit 到 LSB 這區間中全為 1 ,一是 lowbit 到 MSB 這區間全為 1 。最後只須將兩個 mask 進行及運算,取兩者交集,即可得出我們所須的 bitMask 。
如同前面所言,製造一串 1 最快的方法便是先將 1 左移後再 -1 。然而這邊會遇到一個問題,若 highbit 為 63 ,我們必須造出 63 - 0 bit 皆為 1 的 mask 。按照上面的方法,我們首先需要將 1 左移 64 再將其 -1 ,然而對一個 64 bit 的數字進行大於 63 的位移為未定義行為。為了解決這個問題,我們可以首先將其左移 63 bit ,透過 -1 將其變為除 MSB 外其他 bit 皆為 1 的 mask ,再將其左移一位並 + 1 補足缺少一的一次位移。
而 lowbit 的部份則直接位移即可。
### isPalindrome
```c=
/*
* isPalindrome - Return 1 if bit pattern in x is equal to its mirror image
* Example: isPalindrome(0x6F0F0123c480F0F6L) = 1L
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 70
* Rating: 4
*/
long isPalindrome(long x) {
// 0x00000000ffffffff
long mask = ~((1L << 63) >> 31);
long prefix = (x >> 32) & mask;
long postfix = x & mask;
long rev_mask = (0xff << 8) | 0xff;
// 0x0000ffff 0xffff0000
prefix = ((prefix & rev_mask) << 16) | ((prefix & (~rev_mask)) >> 16);
// 0x00ff00ff 0xff00ff00
rev_mask = (0xff << 16) | 0xff;
prefix = ((prefix & rev_mask) << 8) | ((prefix & (~rev_mask)) >> 8);
// 0x0f0f0f0f 0xf0f0f0f0
rev_mask = (0xf << 8) | 0xf;
rev_mask = (rev_mask << 16) | rev_mask;
prefix = ((prefix & rev_mask) << 4) | ((prefix & (~rev_mask)) >> 4);
// 0x33333333
rev_mask = (0x33 << 8) | 0x33;
rev_mask = (rev_mask << 16) | rev_mask;
prefix = ((prefix & rev_mask) << 2) | ((prefix & (~rev_mask)) >> 2);
// 0x55555555
rev_mask = (0x55 << 8) | 0x55;
rev_mask = (rev_mask << 16) | rev_mask;
prefix = ((prefix & rev_mask) << 1) | ((prefix & (~rev_mask)) >> 1);
return !(prefix ^ postfix);
}
```
核心概念將 64 bit 切割為 prefix / postfix 兩子位元串,並將 prefix 以 bit 為單位反轉後,判斷其是否與 postfix 相等。
為首先將 prefix 以 16 bit 為區間,將相鄰的兩 16 bit 區間兩兩互換。接著將區間減半,以 8 bit 為區間,同樣將相鄰的兩區間互換。依此類推,直到區間縮小至 1 bit 為止。這樣就可以將 prefix 以 bit 為單位反轉。
實行方法也十分簡單,只須製作對應區間長度的 mask 並透過及運算分別提相鄰的兩個區間,在透過位移來交換兩者的位置,最後再將他們透過或運算重新組合即可。
實際上,冗長的程式只是為了滿足題目對常數的要求,因此需要花費較多的操作在建構每一階段的 mask 上。
最後將反轉完的字串透過 xor 運算比較即可。
### trueFiveEighths
```c=
/*
* trueFiveEighths - multiplies by 5/8 rounding toward 0,
* avoiding errors due to overflow
* Examples:
* trueFiveEighths(11L) = 6L
* trueFiveEighths(-9L) = -5L
* trueFiveEighths(0x3000000000000000L) = 0x1E00000000000000L (no overflow)
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 4
*/
long trueFiveEighths(long x) {
/*
* For positive
* x * 5 / 8 = x * (4 + 1) / 8 = (x * 4 + x) / 8
* = x / 2 + x / 8
* (x / 2 - e1) + (x / 8 - e2), e1 + e2 may greater than 1
* e1 = 0.(x & 1), e2 = 0.(x & 0x7)
*/
long neg = !!(x >> 63);
long e1 = (x & 1L) << 2;
long e2 = (x & 7L);
long frac = e1 + e2;
long carry = frac >> 3;
long round = (!!(frac & 7)) & neg;
return (x >> 1) + (x >> 3) + carry + round;
}
```
首先,我們嘗試拆解算式 $x * \frac{5}{8} = x * \frac{4 + 1}{8} = \frac{x * 4}{8} + \frac{x}{8} = \frac{x}{2} + \frac{x}{8}$ 。
因此我們似乎只需要計算 $(x >> 1) + (x >> 3)$ 即可。
然而,在整數除法中,小數的部份會被無條件捨去。因此一旦 $\frac{x}{2}, \frac{x}{8}$ 的小數部份相加大於 1 時,計算的答案便會有誤差。因此,在位移前,需要先行檢查會被捨去的部份。需要的檢查有二。
1. 檢查分數負份相加是否會進位。
2. 檢查分數部份是否不為零。若不為零,且 x 為負數,則需要額外的進位。(因為規則為往 0 進行捨入)。
首先,我們提取 $\frac{x}{2}$ 與 $\frac{x}{8}$ 中會被捨去的部份,並對齊。
程式中的 `frac` 即為在運算後會被捨去的數字。由於有經過對齊,所以 `frac` 的小數點位於第 2, 3 bit 之間。因此,我們可以透過右移來將小數點歸位,並得出 `carry` ,也就是計算後由 $\frac{x}{2}, \frac{x}{8}$ 小數部份所組成的進位。
接著,來處裡負數時的進位問題。思路與 [dividePower2](#dividePower2) 類似,透過判斷低 3 位元是否不全為 0 來決定是否需要做進位。
最後只需要將除法的結果與 `carry` , `round` 相加即可得出正確的答案。
### logicalNeg
```c=
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3L) = 0L, logicalNeg(0L) = 1L
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
long logicalNeg(long x) {
long tmin = 1L << 63;
x += tmin;
// abs
long mask = x >> 63;
x = (x ^ mask) + (mask & 1);
// Now x is negative only if x is originally 0
x >>= 63;
x = ~x;
x += 1;
return x;
}
```
題目的首要目標便是檢測 x 是否為零。這邊利用的技巧為:在二的補數中,只有 TMIN 在取絕對值後會為負數。
因此,首先我們將 x 加上 TMIN 。唯有當 x 原本為零時, x 此時會為 TMIN 。接著透過對 x 做絕對值運算,再判斷 x 是否為負數即可反推出 x 原本是否為零。
若 x 原本為零,則取完絕對值後, x 會為負數。因此在 x 右移 63 bit 後會為 0xffffffffffffffff (-1) ,否則此時 x 會為 0 。透過將 x += 1 ,即可將 x 變為符合題目要求的格式,當 x 原本為零時, !x 為一。反之, !x 為零。