# 2020q3 Homework3 (quiz3)
contributed by < `iamchiawei` >
> [2020q3 第 3 週測驗題](https://hackmd.io/@sysprog/2020-quiz3)
###### tags: `sysprog2020`
## 測驗一
### 題目解析
```cpp
int asr_i(signed int m, unsigned int n)
{
const int logical = (((int) -1) OP1) > 0;
unsigned int fixu = -(logical & (OP2));
int fix = *(int *) &fixu;
return (m >> n) | (fix ^ (fix >> n));
}
```
程式碼中,為實作跨平台 ASR,首先須分辨當前環境的 ASR 為邏輯 / 算數右移,而有效處理方式即在當前環境測試並利用其判斷結果計算。因此此處利用一個全部位元皆為 1 的有號負數 -1,向右位移一個 bit,
1. 若預設為算數位移,sign bit 應為 1,因此結果應仍 < 0
2. 若預設為邏輯位移,sign bit 應為 0,因此結果應 > 0
```cpp
const int logical = (((int) -1) OP1) > 0;
```
因此 **OP1 應選擇 `(b)` `>> 1`**,向右位移一個 bit。
```cpp
unsigned int fixu = -(logical & (OP2));
```
在此行中,由於 `logical` 值應為 0 或 1,取決於系統預設為算數或邏輯位移,因此 `-(logical & (OP2))` 則應會是 0 或 -1 (0xffffffff)。再看到回傳值對 `(fix ^ (fix >> n))` 做 OR 運算,可推測應是欲利用此數,當系統預設為邏輯位移時可以將負數位移所需的每個 1 補回去。
若 `logical` 為 0,表示預設為算數位移,則 `m >> n` 即已正確,且此時 `fix` 為 0,不影響結果。
若 `logical` 為 1,表示預設為邏輯位移,此時當 `m` 為正,`fixu` 須為 0x00000000,反之則為 0xffffffff。因此 **OP2 應選擇 `(c)` `m < 0`**,使 `fixu` 在 `m` 為負數時為 0xffffffff。
### 實作其他資料寬度的 ASR
實作程式碼:
```cpp
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define asr_i(m, n) \
_Generic((m), int8_t \
: asr_i8, int16_t \
: asr_i16, int32_t \
: asr_i32, int64_t \
: asr_i64)(m, n)
#define asr(type) \
const type logical = (((type)-1) >> 1) > 0; \
u##type fixu = -(logical & (m < 0)); \
type fix = *(type *)&fixu; \
return (m >> n) | (fix ^ (fix >> n));
int8_t asr_i8(int8_t m, unsigned int n) { asr(int8_t); }
int16_t asr_i16(int16_t m, unsigned int n) { asr(int16_t); }
int32_t asr_i32(int32_t m, unsigned int n) { asr(int32_t); }
int64_t asr_i64(int64_t m, unsigned int n) { asr(int64_t); }
```
參照 [你所不知道的 C 語言:前置處理器應用篇](/@sysprog/c-preprocessor) 中 `_Generic` 用法撰寫 `asr_i` 函式的通用巨集,並分別使用依照各資料寬度的版本。為精簡程式碼,再定義一 `asr` 替代共用程式碼,並利用 `type` 即 `##` 做字串連接及轉換。
## 測驗二
### 題目解析
[ LeetCode 342. Power of Four](https://leetcode.com/problems/power-of-four/)
```cpp
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) OPQ);
}
```
* `num > 0` :檢查 `num` 是否大於 0,若否則不可能為 $4^n$
* `(num & (num - 1)) == 0` : 檢查 `num` 是否為 $2^n$,若否則不可能為 $4^n$
因此 `!(__builtin_ctz(num) OPQ)`,必為檢查 `num` 是否為 $4^n$,首先先觀察 $4^n$ 的二進位編碼:
| Decimal | Binary |
|---------------|--------------|
|$4^0$ |0b1 |
|$4^1$ |0b100 |
|$4^2$ |0b10000 |
|$4^3$ |0b1000000 |
由上表可看出 `__builtin_ctz(num)` 若為 2 的倍數,則 `num` 即是 $4^n$。
因此 **OPQ 應選擇 `(f)` `& 0x1`**,
若是尾部 0 的數量為 2 的倍數,則 `!(__builtin_ctz(num) & 0x1)` 為 1,否則為 0。
### 改寫程式碼
重新釐清判斷所需條件,可簡化為以下兩項:
* 僅有一個 1-bit
* 右側 0-bits 為偶數個
因此可換成以下等價寫法:
```cpp
bool isPowerOfFour(int num){
return !(__builtin_popcount(num) ^ 0x1) && (__builtin_ffs(num) & 0x1);
}
```
* `!(__builtin_popcount(num) ^ 0x1)` 為確認僅有 1 個 1-bit。
* `__builtin_ffs(num) & 0x1` 為確認右側 0-bit 共偶數個,
此式即等價於題目中提供的 `!(__builtin_ctz(num) & 0x1)`。
附上在 leetcode 的執行結果:
![](https://i.imgur.com/csMzZeZ.png =600x)
### 練習 LeetCode [1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/)
```cpp
int bitwiseComplement(int N){
return N ? ~N ^ (0xFFFFFFFF << (32 - __builtin_clz(N))) : 1;
}
```
* 首先須確認當前數字的長度,因此先計算出左側 0-bits 的數量 `(32 - __builtin_clz(N))`
* 製作 bitmask `(0xFFFFFFFF << (32 - __builtin_clz(N)))`,
對其做 XOR 運算則將可保持數字長度以外的位元皆為 0,而長度內則保持其原值。
* 接著將 N 反轉並對 XOR bitmask,即可得正解。
附上在 leetcode 的執行結果:
![](https://i.imgur.com/9zUOmdF.png =600x)
### 練習 LeetCode [1009. Complement of Base 10 Integer](https://leetcode.com/problems/first-missing-positive/)
```cpp
#define XOR_SWAP(a, b) ((a) ^= (b), (b) ^= (a), (a) ^= (b))
int firstMissingPositive(int* nums, int numsSize){
for (int i = 0; i < numsSize; ++i) {
if (nums[i] > 0 && nums[i] <= numsSize) {
int pos = nums[i] - 1;
if (nums[i] != nums[pos] && i != pos) {
XOR_SWAP(nums[pos], nums[i]);
i--;
}
}
}
for (int i = 1; i <= numsSize; ++i) {
if (nums[i - 1] != i) return i;
}
return numsSize + 1;
}
```
附上在 leetcode 的執行結果:
![](https://i.imgur.com/l7btNVZ.png =600x)
## 測驗三
### 題目解析
```cpp
int numberOfSteps (int num)
{
return num ? AAA + 31 - BBB : 0;
}
```
* `__builtin_popcount(num)` 為 `num` 中 1-bits 的數量。
* `__builtin_clz(num)` 為 `num` 由左至最左 1-bit 的 0-bit 數量。
* 由演算法中可看出將有兩種情況:
1. `num` 為偶數 (last bit is 0):`num >>= 1`
2. `num` 為奇數(last bit is 1):`num -= 1`
* 而若遇情況 2 後,必須接續一次情況 1,且情況二正好為偵測到一次 1-bit,此組合發生次數會是 1-bit 的數量,因此可知道至少須 (`__builtin_popcount(num)` * 2) steps。
* 而獨立情況 1 發生次數會是 0-bit 的數量,因此需要 (31 - `__builtin_popcount(num)`) steps,由於是非負有號整數,最左 bit 必為 0 而不採計。
* 最後若左側已有數個 0-bit,則當 1-bit 已消耗完則可提早結束,即少了 `__builtin_clz(num)` steps。
* 因此將以上條件加總,可得最終所需步數為:
`__builtin_popcount(num)` + 31 - `__builtin_clz(num)`
* **AAA 應選擇 `(a)` `__builtin_popcount(num)`**
* **BBB 應選擇 `(b)` `__builtin_clz(num)`**
## 測驗四
### 題目解析
原程式碼:
```cpp
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
while (v) {
uint64_t t = v;
v = u % v;
u = t;
}
return u;
}
```
改寫程式碼:
```cpp
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v) {
if (!u || !v) return u | v;
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (XXX);
return YYY;
}
```
* 考慮若兩數若具公因數 $2^n$,直接移除尾部相同數量的 0-bits 將快過原程式碼中兩數交互餘除的方式,因此首先逐一確認兩數尾部是否皆為零,並將其數量儲存為 `shift` 變數。
```cpp
int shift;
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
* 若 `u` 仍為 2 的倍數,可先除去因數 2 減少運算量,$gcd(u', v') = gcd(u' / 2, v')$。
* 若 `v` 仍為 2 的倍數,可先除去因數 2 減少運算量,$gcd(u', v') = gcd(u', v' / 2)$。
* 接著即是輾轉相除法,且迴圈中判斷式可確保每次運算完 `v` 皆是較小數。
* 根據輾轉相除法,當一數為零時,另一非零數即為最大公因數,由上一敘述可知 `v` 微較小數,因此此迴圈判斷式 **XXX 應是 `(b)` `v`**。
* 最後可得去除公因數 $2^n$ 後的 $gcd(u', v')$,即 `u` 值,因此再將其乘以先前所得 $2^n$ 即為正解,**YYY 應選擇 `e` `u << shift`**。
### 透過 __builtin_ctz 改寫 GCD
實作程式碼:
```cpp
#ifndef min
#define min(x, y) ((x) < (y) ? (x) : (y))
#endif
uint64_t faster_gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift = min(__builtin_ctz(u), __builtin_ctz(v));
u >>= shift; v >>= shift;
while (!(u & 1))
u >>= 1;
do {
while (!(v & 1))
v >>= 1;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
* 使用 __builtin_ctz 取代並加速 0-bits 數量計算,並且 bits 的右移僅需執行一次。
## 測驗五
### 題目解析
原程式碼:
```cpp
#include <stddef.h>
size_t naive(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
for (size_t k = 0; k < bitmapsize; ++k) {
uint64_t bitset = bitmap[k];
size_t p = k * 64;
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
}
return pos;
}
```
改寫程式碼:
```cpp
#include <stddef.h>
size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out)
{
size_t pos = 0;
uint64_t bitset;
for (size_t k = 0; k < bitmapsize; ++k) {
bitset = bitmap[k];
while (bitset != 0) {
uint64_t t = KKK;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
* 由 `naive` 可看出功能為逐一比對 `bitmap` 中各元素的每一個 bit,將 bit 為 1 的位置紀錄至 `out` 中,並且 `pos` 最終即為 1-bits 的數量。
* 而 `improved` 中則是利用 `__builtin_ctzll` 直接得到尾部 0-bits 的個數,藉此省去逐一比對的時間。
* 依迴圈內容,`bitset` 更新只依賴 `bitset ^= t`,並應**將由右第一個 1-bit 改為 0**,`__builtin_ctzll(bitset)` 才可依序算出每一個 1-bit 的位置。
* 看到選項中的 `bitset & -bitset`,`-bitset` 等同於 `~bitset + 1`,即是原最右 1-bit 變為 0,及其右方所有 0-bits 全部替換為 1,此時再加 1 則會進位至原最右的 1-bit,而其左方其餘 bits 則皆與原數相反,接著做 AND 運算後則僅留下原數由右至左第一個 1-bit。
> ex.
> x = 0b01011000
> ~x = 0b10100111
> -x = ~x + 1 = 0b10101000
> x & -x = 0b00001000
* 因此 **KKK 應選擇 `(e)` `bitset & -bitset`**