owned this note
owned this note
Published
Linked with GitHub
# 2020q3 Homework4 (quiz4)
contributed by `joey3639570`
###### tags: `進階電腦系統理論與實作`
## 測驗 `1`
```cpp=
int hammingDistance(int x, int y) {
return __builtin_popcount(x OP y);
}
```
### 延伸問題:
>解釋上述程式碼運作原理,並舉出不使用 gcc extension 的 C99 實作程式碼;
hammingDistance 要找出有幾個位元的不同,`__builtin_popcount` 用於找出有幾個位元的 `1` , 利用`x ^ y` 能找出位元值不同者,以下面例子而言:
```
1 (0 0 0 1)
8 (1 0 0 0)
[ ] [ ]
| |
\_ 2 _/
```
`0001` ^ `1000` = `1001`
`__builtin_popcount(1001)` = **2**
答案: `OP` 為 `^`
#### 不使用 GCC Extension
主要避免使用的部分為 `__builtin_popcount()`
參考: [Bits Twiddling Hacks : Counting bits set, Brian Kernighan's way](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan)
```cpp=
int hammingDistance(int x, int y) {
int res, n;
n = x ^ y;
for (res = 0; n; n &= n-1) {
res++;
}
return res;
}
```
利用 `n &= n-1` 清除對 the least significant bit set,此方法有機會比直覺的方法(純迴圈)快,仍有用到分支,仍有改善空間。
>練習 LeetCode 477. Total Hamming Distance,你應當提交 (submit) 你的實作 (C 語言實作),並確保執行結果正確且不超時
### 題目解說:
>Explanation: In binary representation, the 4 is 0100, 14 is 1110, and 2 is 0010 (just showing the four bits relevant in this case). So the answer will be:
HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6.
### 直覺方法:
```c=
int hammingDistance(int x, int y) {
return __builtin_popcount(x ^ y);
}
int totalHammingDistance(int* nums, int numsSize){
int i = 0;
int j;
int result = 0;
for (i ; i < numsSize; i++) {
for (j = i + 1; j < numsSize; j++) {
result += hammingDistance(nums[i], nums[j]);
}
}
return result;
}
```
果不其然,超時
![](https://i.imgur.com/kz5TeRG.png)
>研讀錯誤更正碼簡介及抽象代數的實務應用,摘錄其中 Hamming Distance 和 Hamming Weight 的描述並重新闡述,舉出實際的 C 程式碼來解說;
>- 搭配閱讀 Hamming codes, Part I 及 Hamming codes, Part II,你可適度摘錄影片中的素材,但應當清楚標註來源
>研讀 Reed–Solomon error correction,再閱讀 Linux 核心原始程式碼的 lib/reed_solomon 目錄,抽離後者程式碼為單獨可執行的程式,作為 ECC 的示範。
## 測驗 `2`
```cpp=
#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` 可以 `obj->parent = malloc(n * sizeof(int *));` 推論出來,其對於 `obj->parent` 分配了 n 個 `int *` 的空間,這表示 parent 為這些指標們的指標,故 `AAA` 為 `int **parent`
在此可以先了解 `TreeAncestor` 的結構:
- `int **parent` : 可以參考題目解釋,如下
>給你一棵樹,樹上有 n 個節點,編號自 0 到 n−1。樹以父節點陣列的形式提供,其中 parent[i] 是節點 i 的父節點。樹的根節點是編號為 0 的節點。
- `max_level` : 記錄著總共有幾層的樹
`treeAncestorCreate` :
1. 先 malloc 出 `TreeAncestor` 及 `obj->parent` 的記憶體空間
2. 設置 `max_level` 為 `32 - __builtin_clz(n) + 1` ,共 32 位元的狀況下,減掉 leading zero 能得到 Binary Tree 的共有幾層。以 `N` 為 15 (1111) 為例,第一層有 $2^0$ 個, 第二層有 $2^1$ 個,第三層有 $2^2$ 個,第四層有 $2^3$ 個, `32 - __builtin_clz(n) + 1` 為 `32 - 28 + 1` 可以參考下圖 :
```graphviz
digraph G {
a [label="0"]
b [label="1"]
c [label="2"]
d [label="3"]
e [label="4"]
f [label="5"]
g [label="6"];
h [label="7"];
i [label="8"];
j [label="9"];
k [label="10"];
l [label="11"];
m [label="12"];
n [label="13"];
o [label="14"];
a->b;a->c;
b->d;b->e;c->f;c->g;
d->h;d->i;e->j;e->k;
f->l;f->m;g->n;g->o;
labelloc="t";
label="My Graph";
}
```
3. 接下的 for 迴圈,會對於每個 `obj->parent[i]` 分配 `max_level` 個 `int` 的空間,並且初始化這些數值為 -1
4. 下一個 for 迴圈,會對於每個 `obj->parent[i]` 的第 0 個設為 `parent[i]` ,也就是該節點自己本身
5. 再下一個 for 迴圈,利用一 flag `quit` 為迴圈終止條件,藉此建立每個節點的 ancestor 關係
6. 最後 `obj->max_level = max_level - 1`,再把前面多的減一弄回來
:::info
待辦:
- 為何 max_level 一開始 要加 1 ? (``32 - __builtin_clz(n) + 1``)
> 程式碼是寫給你改進的,裡頭故意留下一些缺失,遇到疑慮就大膽假設小心求證
> :notes: jserv
:::
`treeAncestorGetKthAncestor` :
直接探討到 for 迴圈:
- 在 i 未到達 `max_level` 且 node <s>本身有被賦予值</s> 已指定數值,迴圈持續進行,找到正確的階層 (k) ,便會指派 ` obj->parent[node][i]` 給 `node`
:::warning
"assignment" 應依據文意翻譯,若書寫為「賦值」往往會詞語不通順
:notes: jserv
:::
>在 treeAncestorCreate 函式內部,若干記憶體被浪費,請撰寫完整測試程式碼,並透過工具分析;
>在 LeetCode 1483. Kth Ancestor of a Tree Node 提交你的實作,你應該要在 C 語言項目中,執行時間領先 75% 的提交者;
## 測驗 `3`
```cpp=
#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;
}
```
### 延伸問題:
>解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
> - 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
- `is_divisible` 及 `M3` 及 `M5` 這部分可以回歸到 quiz2 的測驗 `4` 參考
- `length` 是要 print 出的字串長度,共有以下這幾個可能 :
- `FizzBuzz` : 8
- `Fizz` 及 `Buzz` : 4
- print 數字的狀況下, `length` 會被設為 2,這是整數格式字串 `"%d"` 的長度
- `fmt[MSG_LEN + 1]` 要注意字串有 `'\0'` ,所以要 +1
- 以四種字串可能探討 `strncpy(fmt, &"FizzBuzz%u"[(MSG_LEN >> KK1) >> (KK2 << KK3)], length);`
- 先以下方表格整理字串狀況及對應的變數 :
| 字串 | 開始 Index | div3 | div5 |
|:---------- |:----------:|:----:|:----:|
| `FizzBuzz` | 0 | 1 | 1 |
| `Fizz` | 0 | 1 | 0 |
| `Buzz` | 4 | 0 | 1 |
| 數字 | 8 | 0 | 0 |
綜合上面判斷 `KK1` `KK2` `KK3` :
- `KK1` : 使 Index 有 8 及 4 的可能,倘若只有用到 `KK1` ,字串會是 `Buzz` ,由此推論是 `div5`
- `KK2` 及 `KK3` : 這兩個要一起看,當需要 print 出數字時, Index 要保留為 8 , 故 `KK2 << KK3` 要為 0 ,綜合 print `Buzz` 和數字的 Case 推出 `KK2` 為 `div3` , `KK3` 為 `2`
- 最後要把字串的尾端放上 `'\0'`
>評估 `naive.c` 和 `bitwise.c`
`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;
}
```
- 首先較顯而易見的是這 `bitwise.c` 大幅減少了分支的使用,這於先前我的[作業 2](https://hackmd.io/@joey3639570/Hkoajm0Vw)有提過減少分支的影響及資安問題
:::info
待辦:
- 效能評估
:::
>分析 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
## 測驗 `4`
```cpp=
#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
}
```
### 延伸問題:
>解釋程式運算原理,可搭配 Microsoft Research 的專案 snmalloc 來解說;
對應的原始程式碼 src/mem/sizeclass.h
>練習 LeetCode 51. N-Queens,應選擇 C 語言並善用上述的 __builtin_ffs,大幅降低記憶體開銷;