# 2020q3 Homework3 (quiz3)
contributed by < `eecheng87` >
###### tags: `進階電腦系統應用2020`
## 測驗 1
```cpp
int asr_i(signed int m, unsigned int n)
{
const int logical = (((int) -1) >> 1
) > 0;
unsigned int fixu = -(logical & (m < 0));
int fix = *(int *) &fixu;
return (m >> n) | (fix ^ (fix >> n));
}
```
運作原理:
`asr_i` 是為了避免 unspecified behavior,分別為負數右移補零或一,這裡希望不管怎樣都補一。從 `return` 的結果來看可以發現如果本來就補一那就回傳 `(m >> n)`,即什麼都不用改。反之則透過 `(fix ^ (fix >> n))` 修正。`logical` 是用來表示是否需要修正,如果負數右移補 1 則 `logical` 為 0,不須修正。如果需要修正則 `logical` 為 1,且 `fixu` 為 $11111..1$。`(fix ^ (fix >> n))` 為 $10000..0$,如此一來修正後剛好可以讓 most significant bit 變成 1。
## 測驗 2
```cpp
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_clz(num) OPQ);
}
```
運作原理:
`(num & (num - 1))==0` 是用來檢查是否為 2 power。`!(__builtin_clz(num) & 0x1)` 是用來檢查該 bit 所在的位置是否為偶數。(4 的 power,1 一定在偶數位)
## 測驗 3
```cpp
int numberOfSteps (int num)
{
return num ? __builtin_popcount(num)
+ 31 - __builtin_clz(num) : 0;
}
```
運作原理:
對 `num` 的運算有兩種,一是是偶數右移,二是奇數減一。`__builtin_clz(num)` 代表至少還要運算幾次,若 leading one 後均為 0,那答案就是 `31 - __builtin_clz(num)`。如果 leading one 後還有 1,那右移到 least significant bit 為 1 時就要停下來做 -1 的運算(答案加一),而此時 `num` 又會繼續變偶數右移。可以發現如果 leading one 後有幾個 1 就要再多做幾個運算(減一),所以最答案的公式還要再加 `__builtin_popcount(num)`。
## 測驗 4
```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 (v);
return u << shift;
}
```
運作原理:
可利用已知的 gcd 相關知識來解題
* If both x and y are 0, gcd is zero gcd(0, 0) = 0.
* gcd(x, 0) = x and gcd(0, y) = y because everything divides 0.
* If x and y are both even, gcd(x, y) = 2*gcd(x/2, y/2) because 2 is a common divisor. Multiplication with 2 can be done with bitwise shift operator.
* If x is even and y is odd, gcd(x, y) = gcd(x/2, y).
* On similar lines, if x is odd and y is even, then gcd(x, y) = gcd(x, y/2). It is because 2 is not a common divisor.
一開始如果 `u`, `v` 均為偶數,那可以確定他們一定有公因數 2,用 `shift` 來記有幾個 2。接著 `while` 內就是輾轉相除法的實現,直到 `u == v` 跳出迴圈(這裡還有先處理一奇一偶的狀況,此狀況的公因數必不含 2,所以可以把偶數 shift,直到變成奇數)。
## 測驗 5
```cpp
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 = bitset & -bitset;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
運作原理:
本題最神的地方就是利用 `bitset & -bitset` 找出目前最低位的 1,並紀錄到 `t`。舉例來說,若目前 `bitset` 為 $000101_b$,那 `t` 就會變成 $000001_b$,接著就可以透過 XOR 把該位的 1 清掉,其他保留(XOR 的特性)。
註:網路上對 `__builtin_ctzll` 的解說有點容易讓人誤解,其行為為回傳由低位往高位遇上連續多少個 0 才碰到 1。
就速度方面,改善後的版本在每次 `while` 都可以直接找到下一個 1 做紀錄(一次跳過多個 0),相形之下,`naive` 版本在每個 k 都要迭代 64 次。
效能比較:
根據不同的 bitmap 分佈來做實驗
某區段特別集中的效能比較,可發現 `improved` 略優
```cpp
void centralize_mp(uint64_t *mp) {
for (int i = 0; i < MPSIZE; i++) {
if ((i % 3) == 0) {
mp[i] = 0xFFFFFFFFFFFFFFFF;
}
}
}
```
![](https://i.imgur.com/yiIRDso.png)
交錯分佈的 bitmap,兩種方法就差異較小,因為 `improved` 的內層迭代次數和 `naive` 拉進。
```cpp
void interleave_mp(uint64_t *mp) {
for (int i = 0; i < MPSIZE; i++) {
mp[i] = 0xAAAAAAAAAAAAAAAA;
}
}
```
![](https://i.imgur.com/k8GbSR6.png)
所以簡單分類的話,如果你的 bitmap 越鬆散(1 越少),那用 `improved` 的效益就更高。