# 2020q3 Homework3 (quiz3)
contributed by < ``yceugene`` >
###### tags: `linux2020` `sysprog2020` `quiz`
[題目連結](https://hackmd.io/@sysprog/2020-quiz3)
## :memo: 目錄
[TOC]
## 1.有號整數的跨平台 ASR 實作
```c
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));
}
```
* 第 1 行是為了判斷編譯器的右移型態, 已知 -1 的二補數表示為 ``0xFFFFFFFF`` ,若``0xFFFFFFFF``在右移後得到正數,表示編譯器為邏輯右移(最高位補 0);反之則為算術右移(符號位元右移),而位移結果的正負號判斷就是和 0 比大小,因此 ``OP1 = >>1``。(logical = 0 或 1 代表編譯器為邏輯右移或算術右移)
* 第 2 行是為了判斷 m 的正負號,只有當 m 為負數時才需考慮位移的正確性(正數位移,兩種右移型態都會從最高位元補 0,不影響結果正確性)。
| 右移型態 | m 的正負號| 正確性 |
| -------- | -------- | -------- |
| 邏輯右移,即 ``logical==1`` | ``(m < 0)==1 `` | 錯誤|
| 邏輯右移,即 ``logical==1`` | ``(m < 0)==0`` | 正確|
| 算術右移,即 ``logical==0`` | ``(m < 0)==1`` | 正確 |
| 算術右移,即 ``logical==0`` | ``(m < 0)==0`` | 正確 |
由上表可知,我們只需要考慮邏輯右移且 ``m < 0`` 的情況,在此種情況下 ``fixu == 0xFFFFFFFF``, 其他三種情況下 ``fixu == 0x00000000``
* 第 3 行 ``int fix = *(int *) &fixu;`` 是將 fluxu 無號數)轉為 flux(有號數)
* 第 4 行是為了針對上述錯誤情況(負數的邏輯位移)做修正,從最高位元開始,將補 0 的位元全部改為 1。
* 解答:
``OP1 = >>1``
``OP2 = (m < 0)``
## 2.實作 Power of Four 的判斷
```c
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) OPQ);
}
```
想法: 若一個數字 num 為 4 的冪次方,則其二進制表示式中會有以下特性:
* 由於 num 必為 2 的冪次方,因此其二進制中恰有 1 個位元值為 1,其餘位元值為 0,而``(num & (num - 1))==0`` 就是用來判斷這個條件。
* num 的二進制中,位元值為 1 的位元,從該位元由其右邊一個 bit 一路向右延伸到底,一共會經過偶數個值為 0 的 bit,而 ``(__builtin_ctz(num) = & 0x1)`` 就是用來判斷 num 二進制式末尾是否有偶數個值為 0 的 bit。
* 解答:
* ``OPQ = & 0x1``
## 3.LeetCode 1342. Number of Steps to Reduce a Number to Zero
* 題意: 針對輸入的 32bit int,需做多少次計算,才能將輸入變為 0。(計算條件為: 當正整數為偶數時,要對其做除以 2;當正整數為奇數時,要對其 -1。)
* 程式碼列表如下: (運用 gcc 內建函式實作,假設 int 寬度為 32 位元)
```
int numberOfSteps (int num)
{
return num ? AAA + 31 - BBB : 0;
}
```
* 根據題意, 計算方式可以拆解為:
* 當正整數為奇數時,把數值的 LSB 從 1 改為 0。
* 當正整數為偶數時,找到數值最右邊不為 0 的位元,把數值邏輯右移直到該位元變成數值的 LSB 。
* 程式碼原理:
因此總計算次數可以想成:
1. **數值有多少個 1 ?** (這些 1 會依序被改成 0)
可使用 __builtin_popcount(num) 做到。
2. **要做幾次邏輯右移?**
數值是 32 bit 正整數,最壞的情況是邏輯右移 31 次。但其實只要知道數值二進位表示中,從 MSB 往右數來第一個值為 1 的位元是哪一個位元即可,也就是計算 `31 - __builtin_clz(num)`。
* 答案":
* AAA = `__builtin_popcount(num)`
* BBB = `31 - __builtin_clz(num)`
## 4.64-bit GCD (greatest common divisor) 求值
* 題意:
* 原程式碼:
``` c
#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;
}
```
* 等價實作程式碼:
``` c
#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;
}
```
### 程式原理:
如果 u、v 皆為偶數,就把 u、v 各除以 2,以 `shift` 紀錄除以 2 的次數。
`((u | v) & 1)` 是用來判斷 u、v 是否都為偶數。 (`& 1` 是用來取出數值的 LSB,LSB 為 1 等價於 他是奇數)
``` c
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
}
```
接者分別判斷 u、v 的奇偶性,若有一方為偶數,對他除以 2。
``` c
while (!(u & 1))
u /= 2;
do {
while (!(v & 1))
:
:
} while (XXX);
```
對 u、v 做輾轉相除法。
``` c
do {
while (!(v & 1))
v /= 2;
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (XXX);
```
觀察程式碼會發現,每回合輾轉相除法算出的餘數會保留在 v 中,程式是透過餘數是否為 0 來判斷 gcd 是否計算完成,因此 `XXX = v`。程式執行完會回傳 gcd,gcd 的值是輾轉相除得到的商數 'u' 乘以前面是先除去的 2 的冪次方 (shift) 倍數,因此最後應回傳 `YYY = u << shift`。
## 5.bit array (bitset)
* 題意: 以 bitmap 方式處理輸入陣列 (每個元素會對應到 64 個 bit),把所有值為 1 的 bit 對應的位置記下儲存到 `out`。兩分程式碼功能相同,但第二份效率較好。
* 程式碼 1:
``` c
#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;
}
```
上述程式碼中,會逐一比對所有的位元 `if ((bitset >> i) & 0x1)`,判斷值是否為 1,這樣的效率比較差。
* 程式碼 2: (用 clz 改寫為效率更高且等價的程式碼)
``` c
#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;
}
```
上述程式碼是透過 __builtin_ctzll() 直接找到下一個 set bit 的位置,不用再逐一比較所有 bit,因此效率較好。
程式原理:
* __builtin_ctzll() 的作用是計算最右 set bit 到 LSB 之間有多少 0 bit。
* `int r = __builtin_ctzll(bitset);` 這裡的 r 是代表目前的 set bit 是第幾個位元。
* `bitset ^= t;` 是把剛剛紀錄到的 set bit 清除成 0,為了讓下一回合也能用 __builtin_ctzll(bitset) 做判斷。清除的方式是 `bitset & (bitset-1)`。 以 8 位元做考慮舉例如下: 假設 bitset 為 `0b0100 1000`,則: bitset-1 為 `0b0100 0111`;bitset & (bitset-1) 為 `0b0100 0000`。