# 2020q3 Homework4 (quiz4)
contributed by < `Veternal1226` >
###### tags: `sysprog2020`
---
### 測驗 `1`
LeetCode 461. Hamming Distance 提及,兩個整數間的 Hamming distance 為其二進位的每個位元的差。請計算輸入參數兩整數 x 與 y 的 Hamming distance,例如整數 1 的二進位為 0 0 0 1,而整數 4 的二進位為 0 1 0 0,則 1 與 4 的 Hamming distance 為 2。
```
1 (0 0 0 1)
4 (0 1 0 0)
[ ] [ ]
| |
\_ 2 _/
```
![](https://i.imgur.com/4HxzNx7.png)
上圖 hypercube (中文翻譯:超方形) 中,紅色路徑的 `0100` 到 `1001` 的 Hamming distance 為 `3`,而藍色路徑的 `0110` 到 `1110` 的 Hamming distance 則是 `1`。
運用第 3 周測驗 提到的 `__builtin_popcount` (gcc extension 之一),我們可實作如下:
```
int hammingDistance(int x, int y) {
return __builtin_popcount(x OP y);
}
```
請補完程式碼。
OP = (c\) ^
#### 解題想法
Hamming distance 為 1 相當於對 1 bit 做反向,因此只要找出兩數的二進制之間有幾個相異的 bit ,即可求出 Hamming distance。
二進制表示的兩數之間找出相異處可用 XOR (^) 邏輯運算,兩數之間相差的各位元經 XOR 運算後的結果會為 1 ,使用 `__builtin_popcount` 算出共有幾個 bits 為 1 即可求出 Hamming distance 。
因此此題選擇 ==(c\) ^==
:::success
延伸問題:
2. 練習 LeetCode 477. Total Hamming Distance,你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時
:::
第一版寫法: 超時
程式碼:
```cpp=1
int hammingDistance(int x, int y) {
return __builtin_popcount(x ^ y);
}
int totalHammingDistance(int* nums, int numsSize){
int l=0;
int r=0;
int result=0;
for(l=0;l<numsSize-1;l++) {
for(r=l+1;r<numsSize;r++) {
result+=hammingDistance(nums[l],nums[r]);
//printf("l=%d,r=%d\tresult=%d\n",nums[l],nums[r],result);
}
}
return result;
}
```
超時原因: 數組長度最大為 $10^4$ ,因此一組一組找 hamming distance 會導致超時 (共需$\dfrac{(10^4+1)*10^4}{2}=5*10^7$次)。
第二版程式碼: 成功
```cpp=1
int totalHammingDistance(int* nums, int numsSize){
int targetBit=0;
int travel=0;
int oneCount=0;
int result=0;
for( targetBit = 0 ; targetBit < 32 ; targetBit++ ) { //travel 32 bit
oneCount = 0;
for( travel = 0 ; travel < numsSize ; travel++ ) { //count 1 for every number at targetBit
if( (nums[travel] >> targetBit) & 1 ) {
oneCount++;
}
}
result += oneCount * (numsSize - oneCount); //ham dist = (num of 1) * (nums of 0)
//printf("count=%d\t result=%d\n",oneCount,result);
}
return result;
}
```
想法:
因為題目指定數值格式為 int ,針對32位元 (int) 中的其中 1 位元,先數出在此位元下有幾個數組中的數 bit 為 1 ,將 1 的數量乘以 0 的數量即為該 bit 的 hamming distance。最後將 32 bits 都計算完 hamming distance ,加總起來即為答案。
以 4bit 舉例:
|int|3|2|1|0|
|-|-|-|-|-|
|4|0|1|0|0|
|14|1|1|1|0|
|2|0|0|1|0|
|-|-|-|-|-|
|ham|`1*2`=2|`2*1`=2|`2*1`=2|`0*3`=0
加總結果為 `2+2+2+0`=0
結果:
![](https://i.imgur.com/468WEQE.png)
:::success
延伸問題:
1. 解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼;
2.
3. 研讀錯誤更正碼簡介及抽象代數的實務應用,摘錄其中 Hamming Distance 和 Hamming Weight 的描述並重新闡述,舉出實際的 C 程式碼來解說;
- 搭配閱讀 Hamming codes, Part I 及 Hamming codes, Part II,你可適度摘錄影片中的素材,但應當清楚標註來源
4. 研讀 Reed–Solomon error correction,再閱讀 Linux 核心原始程式碼的 lib/reed_solomon 目錄,抽離後者程式碼為單獨可執行的程式,作為 ECC 的示範。
:::
//TODO
---
### 測驗 `2`
LeetCode 1483. Kth Ancestor of a Tree Node 大意:
>給你一棵樹,樹上有 n 個節點,編號自 0 到 $n−1$ 。樹以父節點陣列的形式提供,其中 `parent[i]` 是節點 i 的父節點。樹的根節點是編號為 0 的節點。請設計 `treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)` 函式,回傳節點 node 的第 k 個祖先節點。若不存在這樣的祖先節點,回傳 `-1`。樹節點的第 k 個祖先節點是從該節點到根節點路徑上的第 k 個節點
![](https://i.imgur.com/0cnEhdh.png)
Input:
```cpp
["TreeAncestor","getKthAncestor","getKthAncestor","getKthAncestor"]
[[7,[-1,0,0,1,1,2,2]],[3,1],[5,2],[6,3]]
```
:::info
:information_source: 上方為 JSON 格式,其中:
`7` 表示共有 7 個節點
`GetKthAncestor(3, 1)` 預期回傳 `1`,後者是 `3` 的父節點,即「上 `1` 代」;
`GetKthAncestor(5, 2)` 預期回傳 `0`,後者是 `5` 的祖父節點,即「上 `2` 代」;
`GetKthAncestor(6, 3)` 預期回傳 `-1`,因為不存在這樣的節點
:::
Output:
```cpp
[null,1,0,-1]
```
以下是參考 C 程式實作:
```cpp=1
#include <stdlib.h>
typedef struct {
AAA;
int max_level;
} TreeAncestor;
TreeAncestor *treeAncestorCreate(int n, int *parent, int parentSize)
{
TreeAncestor *obj = malloc(sizeof(TreeAncestor));
obj->parent = malloc(n * sizeof(int *));
int max_level = 32 - __builtin_clz(n) + 1;
for (int i = 0; i < n; i++) {
obj->parent[i] = malloc(max_level * sizeof(int));
for (int j = 0; j < max_level; j++)
obj->parent[i][j] = -1;
}
for (int i = 0; i < parentSize; i++)
obj->parent[i][0] = parent[i];
for (int j = 1;; j++) {
int quit = 1;
for (int i = 0; i < parentSize; i++) {
obj->parent[i][j] = obj->parent[i][j + BBB] == -1
? -1
: obj->parent[obj->parent[i][j - 1]][j - 1];
if (obj->parent[i][j] != -1) quit = 0;
}
if (quit) break;
}
obj->max_level = max_level - 1;
return obj;
}
int treeAncestorGetKthAncestor(TreeAncestor *obj, int node, int k)
{
int max_level = obj->max_level;
for (int i = 0; i < max_level && node != -1; ++i)
if (k & (CCC))
node = obj->parent[node][i];
return node;
}
void treeAncestorFree(TreeAncestor *obj)
{
for (int i = 0; i < obj->n; i++)
free(obj->parent[i]);
free(obj->parent);
free(obj);
}
```
請補完。
作答區
AAA = (b) int **parent
BBB = (b) (-1)
CCC = (f) 1 << i
#### 解題想法
//TODO
:::success
延伸問題:
1. 解釋上述程式碼運作原理,指出可改進的地方,並實作對應的程式碼;
2. 在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析;
3. 在 LeetCode 1483. Kth Ancestor of a Tree Node 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者;
:::
//TODO
---
### 測驗 `3`
:::info
>白板 coding 其實本意 (最好不要是) 不是考一些你已經會的東西,而是考一個你可能不會的問題,然後你要 keep trying, keep doing 下去,因為它的本質是在考一個未來你可能碰到的問題 (而且可能 Google 不到)。
出處: 簡單的 FizzBuzz 藏有深度 (Google 面試題)
:::
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
- 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
- 如果是 5 的倍數,印出 “Buzz”
- 如果是 15 的倍數,印出 “FizzBuzz”
- 如果都不是,就印出數字本身
直覺的實作程式碼如下: (`naive.c`)
```cpp
#include <stdio.h>
int main() {
for (unsigned int i = 1; i < 100; i++) {
if (i % 3 == 0) printf("Fizz");
if (i % 5 == 0) printf("Buzz");
if (i % 15 == 0) printf("FizzBuzz");
if ((i % 3) && (i % 5)) printf("%u", i);
printf("\n");
}
return 0;
}
```
觀察 `printf` 的(格式)字串,可分類為以下三種:
1. 整數格式字串 `"%d"` : 長度為 2 B
2. “Fizz” 或 “Buzz” : 長度為 4 B
3. “FizzBuzz” : 長度為 8 B
考慮下方程式碼:
```cpp
#define MSG_LEN 8
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[start], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
```
我們若能精準從給定輸入 `i` 的規律去控制 `start` 及 `length`,即可符合 FizzBuzz 題意:
```cpp
string literal: "fizzbuzz%u"
offset: 0 4 8
```
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
```cpp=1
#define MSG_LEN 8
static inline bool is_divisible(uint32_t n, uint64_t M) {
return n * M <= M - 1;
}
static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1;
static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1;
int main(int argc, char *argv[]) {
for (size_t i = 1; i <= 100; i++) {
uint8_t div3 = is_divisible(i, M3);
uint8_t div5 = is_divisible(i, M5);
unsigned int length = (2 << div3) << div5;
char fmt[MSG_LEN + 1];
strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
gcc-9 還內建了 FizzBuzz optimization (Bug 82853 - Optimize x % 3 == 0 without modulo)。
請補完。
作答區
KK1 = (a) div5
KK2 = (d) div3
KK3 = (c\) 2
#### 解題想法
根據題目第 17 行`strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);`,得知`&"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)]`的行為是在找要複製字串的起始位置,依照題意正常行為應如下表
|div5|div3|預期字串|起始位置|
|-|-|-|-|
|T|T|FizzBuzz|0|
|T|F|Buzz|4|
|F|T|Fizz|0|
|F|F|%u|8|
先觀察這兩行
|div5|div3|預期字串|起始位置|
|-|-|-|-|
|T|F|Buzz|4|
|F|F|%u|8|
div3 為 0 (false) 的情況下,div5 為 0 起始為 8 ,div5 為 1 起始為 4,因此判斷div5應該是用來向右 shift 1 ,因此推論 ==KK1 = div5==。
起始位置目前為 `(8 >> div5) >> (KK2 << KK3)`
觀察行為表的這兩行
|div5|div3|預期字串|起始位置|
|-|-|-|-|
|F|T|Fizz|0|
|F|F|%u|8|
令 div5 為 0 ,起始位置為 `8 >> (KK2 << KK3)` 。當 div3 為 0 時起始位置為 8 ,此時 `(KK2 << KK3)`為 0;當 div3 為 1 時起始位置為 0 ,此時 `(KK2 << KK3)` 應不小於 4 。選項中只有 0, 1, 2 三種可能的數,因此 `(KK2 << KK3)` 應為 1<<2 或2<<1。
先討論 `(KK2 << KK3)` 為 2<<1的情況:此時 KK2=2, KK3應為 div3 (1) ,但 div3 為 0 時 `(KK2 << KK3)` 為 2 , `8 >> (KK2 << KK3)` 為 2 ,不符合行為。
討論 `(KK2 << KK3)` 為 1<<2的情況: 此時 KK3=2, KK2應為 div3 (1) , div3 為 0 的情況下 `(KK2 << KK3)` 為 0 , `8 >> (KK2 << KK3)` 為 0 ,符合行為
因此判斷 ==KK2 = div3==, ==KK3 = 2==
驗證行為(行為表中均以二進制表示):
(8 >> div5) >> (div3 << 2)
|div5|div3|`(1000 >> div5) >> (div3 << 10)` in bin||
|-|-|-|-|
|T|T|100 >> 100|0|
|T|F|100 >> 0|100 =$(4)_{10}$|
|F|T|1000 >> 100|0|
|F|F|1000 >> 0|1000 =$(8)_{10}$|
:::success
延伸問題:
1. 解釋上述程式運作原理並評估 `naive.c` 和 `bitwise.c` 效能落差
- 避免 stream I/O 帶來的影響,可將 `printf` 更換為 `sprintf`
2. 分析 Faster remainders when the divisor is a constant: beating compilers and libdivide 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 `bitwise.c` 程式碼,試圖運用更少的指令來實作出 branchless;
- 參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware
:::
//TODO
---
### 測驗 `4`
考慮到整數 PAGE_QUEUES 可能有以下數值:
- (1 or 2 or 3 or 4)
- (5 or 6 or 7 or 8) × ($2^n$), n from 1 to 14
給定 `size_t offset`,試著將原本運算:
```cpp
#include <stdint.h>
size_t blockidx = offset / PAGE_QUEUES;
```
由於 `PAGE_QUEUES` 的數值分佈已知,在整數除法時,可思考加速運算的機制。需要注意,某些硬體平台缺乏整數除法指令 (如 Arm Cortex-A8),即使 Intel 公司出品的 Core 處裡器 Sandy Bridge 微架構中,針對 64 位元整數的除法運算,會佔用 40 到 103 個處理器週期,運算成本仍屬沈重。
>來源: Agner Fog’s instruction tables,第 180 頁
於是我們可預先進行計算,從而避免整數除法指令的使用: (假設在 x86_64 硬體執行 GNU/Linux 並透過 gcc/clang 編譯)
```cpp=1
#include <stdint.h>
#define ffs32(x) ((__builtin_ffs(x)) - 1)
size_t blockidx;
size_t divident = PAGE_QUEUES;
blockidx = offset >> ffs32(divident);
divident >>= ffs32(divident);
switch (divident) {
CASES
}
```
其中 CASES 可能形如:
```cpp
case 2: blockidx /= 2;
break;
```
這樣的組合,請用最少的 case-break 敘述做出同等 `size_t blockidx = offset / PAGE_QUEUES;` 效果的程式碼。
參考資料:
- 摘自 Built-in Functions Provided by GCC:
>Built-in Function: int __builtin_ffs (int x)
Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
- Integer division is slow
作答區
CASES 至少該包含哪些數字: (複選)
(b) 3
(d) 5
(f) 7
#### 解題想法
先分析程式碼
```cpp=4
size_t divident = PAGE_QUEUES;
blockidx = offset >> ffs32(divident);
divident >>= ffs32(divident);
```
依照題意,PAGE_QUEUES等價於 $k*2^n$
第四行將PAGE_QUEUES指定為除數
第五行與第六行是將同時向右位移 `ffs32(divident)` 次
`ffs32(divident)` 可用來表示 `divident` 尾巴有n個 0 ,等價於 $k*2^n$ 中的 n。
所以第五行與第六行等價於同時將除數與被除數同除以 $2^n$
從題意得知剛剛定義的 k 只會出現 1~8 ,又因 2, 4, 6, 8 可被 $2^n$ 除掉,且 1 不用額外處理,因此只須處理 3, 5, 7 這三種條件。
故答案為 ==(b) 3 (d) 5 (f) 7==
:::success
延伸問題:
1. 解釋程式運算原理,可搭配 Microsoft Research 的專案 snmalloc 來解說;
- 對應的原始程式碼 src/mem/sizeclass.h
2. 練習 LeetCode 51. N-Queens,應選擇 C 語言並善用上述的 `__builtin_ffs`,大幅降低記憶體開銷;
:::
//TODO