# 2018q3 Homework5 (bits)
contributed by <`AlecJY`>
## 實驗環境
* 作業系統:openSUSE Leap 15.0
* gcc:7.3.1
* Cppcheck:1.8
* clang-format:5.0.1
## `absVal()`
這題在之前的考試有出現過類似的題目,所以一開始很快地寫出這樣的程式碼
``` C
int absVal(int x)
{
int s = x >> 31;
return (x ^ s) + (~s + 1);
}
```
編譯測試結果沒問題,結果在要commit的時候被cppcheck擋了下來
```
[bits.c:114]: (error) Shifting signed 32-bit value by 31 bits is undefined behaviour
```
問題在於對 32-bit signed integer 位移了 32 bits ,所以換個方式思考,將 `x` 轉型成 unsigned integer 再做運算就不會有問題了,所以後來就將程式碼改成底下這樣。
``` C
int absVal(int x)
{
unsigned int *xptr = (unsigned int *) &x;
int sbit = *xptr >> 31;
return (x ^ (~(sbit) + 1)) + sbit;
}
```
和原本最大的差別是原本負數位移 31 bits 會得到 -1 ,現在轉型成無號數之後位移會得到 1 ,所以將原本計算會用到 1 和 -1 的部分互換。
只是這樣會違反題目規定的
> You are expressly forbidden to:
> 6. Use any form of casting.
> 7. Use any data type other than int. This implies that you cannot use arrays, structs, or unions.
參考了一下其他人的作法都是把位移分成兩次執行,所以最後把程式碼改成
``` C
int absVal(int x)
{
int s = x >> 30 >> 1;
return (x ^ s) + (~s + 1);
}
```
總算有通過 cppcheck 的檢查
:::info
翻閱 C99 標準在 6.5.7 第 5 點有底下描述
> The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.
對負數進行位移是根據編譯器的實作所決定的
所以之後去翻閱 GCC 7.3.0 的文件,看到有個選項
```
-Wshift-negative-value
Warn if left shifting a negative value. This warning is enabled by ‘-Wextra’ in
C99 and C++11 modes (and newer).
```
也有解釋為什麼這個 warning 預設是關閉的
```
Shift count operands are probably signed more often than unsigned. Warning about
this would cause far more annoyance than good.
```
還有敘述 GCC 是如何處理有號負數向右位移的
```
Bitwise operators act on the representation of the value including both the sign and
value bits, where the sign bit is considered immediately above the highest-value value
bit. Signed ‘>>’ acts on negative numbers by sign extension
```
:::
## `addOK()`
這題的我的思考方式是這樣
* 兩個正數相加溢出會變成負數
* 兩個負數相加溢出會變成正數或是 0
* 正數和負數相加不會溢位
所以先把 `x` 、 `y` 和他們的和的正負號取出,接下來用 XOR 判斷,如果同時滿足 `x` 、 `y` 正負號相同以及 `x` 、 兩數之和正負號不同就是溢位
## `allEvenBits()` 和 `allOddBits()`
這裡利用位移來把所有的奇數位和偶數位做 AND 運算來算出奇數位或偶數位是否都為 1 ,最後計算完的第 0 bit 就是偶數位的結果,第 1 bit 就是奇數位的結果。
## `anyEvenBit()` 和 `anyOddBit()`
這兩部分跟前面很像,差別只在於只要其中一個 bit 為 1 就好,所以把原本的 AND 運算改成 OR
## `bang()`
這題是在不用 `!` 做到 logical not ,概念上就是有任何 bit 為 1 的話就要回傳 0 ,如果皆為 0 就回傳 1 ,所以用了跟上一題類似的概念,利用位移讓所有位數做 OR ,得到的結果加個 NOT 取最低位數就是需要的結果了
## `bitAnd()`
觀察一下 AND 的真值表
| P | Q | P & Q |
| - | - | ----- |
| 1 | 1 | 1 |
| 1 | 0 | 0 |
| 0 | 1 | 0 |
| 0 | 0 | 0 |
以及 OR 的真值表
| P | Q | P \| Q |
| - | - | ------ |
| 1 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
發現把 AND 真值表的所有值加上 NOT 就變成 OR 的真值表了
## `bitMask()`
這題的概念是先算出要塞幾個 1 進去裡面,之後再把 1 移動到指定的位置,只是現在這種做法會有好幾個undefined behavior。第一個是假如 `diff` 是負數,第四行的 `<< diff` 就會是個 undefined behabior ,再來是對 `~0` 進行位移,因為 `~0` 完之後值會變成 -1 ,對負數向左位移在 C99 標準裡是 undefined behavior ,還有第五行如果 mask 是 0xFFFFFFFF 也會因為對負數位移 0 個位置而產生 undefined behavior 。
``` C=
int bitMask(int highbit, int lowbit)
{
int diff = highbit + ~lowbit + 1;
int mask = ~(~0 << diff << 1);
mask <<= lowbit;
return mask & ~(diff >> 30 >> 1);
}
```
`diff` 是負數這點要解決比較容易,補上底下第四行的程式碼把負數變成無意義的非負數就可以了,向左位移負數的問題則暫時沒想到避免的方法。
``` C=
int bitMask(int highbit, int lowbit)
{
int diff = highbit + ~lowbit + 1;
int udiff = (diff >> 30 >> 1) ^ diff;
int mask = ~(~0 << udiff << 1);
mask <<= lowbit;
return mask & ~(diff >> 30 >> 1);
}
```
## `bitMatch()`
這題是把一樣的bit標出來,也就是 XNOR ,一開始先嘗試用 AND 、 OR 和 NOT 湊出來。
``` C
int bitMatch(int x, int y)
{
return (x & y) | (~x & ~y);
}
```
只是題目不允許使用 OR ,所以再想辦法把 OR 去掉,想法跟 `bitAnd()` 那題差不多
```
int bitMatch(int x, int y)
{
return ~(~(x & y) & ~(~x & ~y));
}
```
## `bitNor()`
這題只是單純的把 OR 去掉,作法跟上一題的後半差不多。
## `bitOr()`
這題跟上面那題一樣也是把 OR 去掉
## `bitParity()`
這題在之前考試有出現過兩次,只是第一次考試有用到 Macro ,第三次考試則是用到了 0x11111111 這個不好湊出來的數,所以用了跟前面 `bang()` 那題一樣的概念,只是差別是把所有 bits 做 XOR,得到的結果就是奇偶較驗的結果。
## `bitReverse()`
這題的第一個想到的想法是利用 divide and conquer 的概念,先 16 bits 一組交換,再 8 bits 一組交換,持續做下去就可以交換完了,只是還沒想到 Mask 要怎麼產生比較好。
## `bitXor()`
`bitMatch()` 那題實際上是在做 XNOR ,所以只要再加一個 NOT 就變成 XOR 了
## `conditional()`
這題允許使用 `!` ,所以先利用 `!` 得到 0 或 1 用來判斷要回傳 y 或是 z,然後如果得到 1 的話就產生 0xFFFFFFFF 的 mask ,如果是 0 則產生值為 0 的 mask,最後將 y 和 z 分別對 NOT mask 以及 mask 做 AND ,這樣就會根據條件讓其中一個的值為零,另一個的值保持不變,最後再用 OR 把他們合併成結果。
``` C
int conditional(int x, int y, int z)
{
int res = !x;
int mask = ~res + 1;
return (y & ~mask) | (z & mask);
}
```
## `copyLSB()`
這題是把每個位元設定成跟最低位元一樣,結果只會有兩種: 0 和 -1 ,先利用 AND 取出最低的位元,然後對取出的位元做互換正負號,就會得到需要的結果。
## `distinctNegation()`
一開始的想法是需要回傳 0 的情況只有 0 和 0x80000000 ,所以湊出下面這個,但是多了一個 operator,而且用到不允許的 `<<`
``` C
int distinctNegation(int x)
{
return !(!x ^ !(x ^ (1 << 31)));
}
```
後來發現自己想太多了,照題意的 x != -x 判斷就可以寫出符合條件的程式碼。先利用二補數的特性算出 -x ,然後再跟 x 做 XOR ,如果結果是 0 就代表完全相等,只是題目有要求不相等需要回傳 1 ,所以再用 `!!` 把回傳值處理成 0 或 1。
``` C
int distinctNegation(int x)
{
return !!(x ^ (~x + 1));
}
```
## `dividePower2()`
這題是將數字除以 2 的 n 次方,這部分在正數沒什麼問題,因為用 `>>` 操作正數在 C99 標準的定義就是處以 2 的 n 次方,但是負數是 implementation-defined ,而這次作業所使用的 GCC 是直接向右移 1 bit 然後在最左方補上 sign bit 。當數字無法整除的時候計算就會有誤差,原本也想說把負數先轉成正數,做完運算再轉回正數,結果也一樣在無法整除時會有誤差,所以後來直接觀察負數除法的行為。以 -241 向右位移 6 以及除以 $2^6$ 為例
```
0b11111111111111111111111100001111 (-241)
0b11111111111111111111111111111101 (-241 / 64)
0b11111111111111111111111111111100 (-241 >> 6)
```
會發現位移 6 的因為餘數問題需要 + 1 才會變成使用除法的答案,多看幾組數字之後會發現有一個簡易的判斷方法,如果我們要除以 $2^n$ ,原來的數字中最低 n bits 中有任何一個 1 就會導致無法整除,因此結果需要 + 1 才會是除法得到的結果,所以用這種想法寫出以下的程式碼
``` C
int dividePower2(int x, int n)
{
int s = x >> 30 >> 1;
int round = !!(x & ~(s << n));
x >>= n;
return x + (round & s);
}
```
步驟就是先取出最左邊的 bit ,如果為正數結果就會是 0 ,如果為負數結果就會是 -1 ,然後利用位移和 NOT 建立 mask ,之後利用 `!!` 將結果轉成 1 或是 0,只是位移負數是未定義行為,但是還暫時想不到其他作法。之後將 `x` 向右位移後加上計算出來是否要加一的值,只是正數沒有加一的問題,所以先將那個值與 `s` 做一次 AND 。
## `evenBits()`
這題要回傳 0x55555555 ,所以就每次 OR 0x55 之後向左位移 8 bits 就可以完成了。
## `ezThreeFourths()`
這題要把數字乘以 3 後除以 4 ,乘以 3 這部分可以把它拆成乘以 2 後加 1 就容易理解多了,只要把數字向左位移 1 後加上數字本身就可以達成乘以 3 的效果,而處以 4 這部分可以利用和 `dividePower2()` 一樣的概念達成。
## `fitsBits()`
這題的想法是先把負數處理成正數會比較好計算,所以先做絕對值,只是例如 -8 可以用 4 bits 表示, 8 則不行,所以做絕對值的時候負數加一的步驟要省略。之後利用位移來檢查是否有超過 n bits 就可以了,只是因為要把 sign bit 考慮進去,所以只要位移 n - 1 bits 就好了。
## `fitsShort()`
這題跟上面那題一樣,只是 n 為定值而已。
## `floatAbsVal()`
這題是浮點數題目,所以少了很多限制,但是老師在作業說明有說到
> 避免用分支 (branch),設計時間複雜度為 $O(1)$ 的實作
所以意思是 `if` 、 `for` 、 `while` 、 `switch` 、 `?` 、 `&&` 、 `||` 都要避免使用,不然就會造成分支
這題是要對浮點數做絕對值,浮點數代表正負號的 bit 在最高位,只要把它寫成 0 就可以達成目標了,只是題目有提到如果輸入的數字是 NaN 要回傳原來的值,所以還要判斷是不是 NaN , 在不用到分支的情況寫出了這樣的程式碼,只是遠遠超過題目所限定的 operater 數。
``` C
unsigned floatAbsVal(unsigned uf)
{
unsigned isnan = (uf >> 23 & 0xFF) == 0xFF;
unsigned frac = !!(uf << 9);
isnan = isnan & frac;
isnan = -isnan;
unsigned mask = ~(1 << 31);
return ((uf & mask) & ~isnan) | (uf & isnan);
}
```