# 2020q3 Homework3 (quiz3)
contributed by < `grb72t3yde` >
###### tags: `sysprog2020`
---
## Links
* [第三週測驗題](https://hackmd.io/@sysprog/2020-quiz3)
* [作業區](https://hackmd.io/@sysprog/2020-homework3)
## 測驗 `1`
依據 ISO/IEC 9899:TC2 (即 C99) 標準的 6.5.7 章節 (第 84 到第 85 頁):
> “The result of E1 >> E2 is E1 right-shifted E2 bit positions. … If E1 has a signed type and a negative value, the resulting value is ==implementation-defined==.”
在 C 語言中,對有號型態的物件進行算術右移 (arithmetic right shift,以下簡稱 ASR 或 asr) 歸屬於 unspecified behavior,包含 Cppcheck 在內的靜態分析器會嘗試找出 C 程式中這項 unspecified behavior。考慮到程式碼的相容性,我們想實作一套可跨越平台的 ASR,假設在二補數系統,無論標的環境的 shift 究竟輸出為何,我們的 ASR 總可做到:
$-5\gg^{arith}1=-3$
上述 $-3$ 正是 $\dfrac{-5}{2}$ 的輸出, 並向 $-\infty$ 進位(rounding)。
針對有號整數的跨平台 ASR 實作如下:
```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));
}
```
### 解題思路
OP1 = ? OP2 = ?
首先看題目給的資訊 :
* 參考 [Unspecified_behavior](https://en.wikipedia.org/wiki/Unspecified_behavior), 得知此種行為是 `source code` 在基於使用不同的`編譯器`或是`編譯器設定`會有不同的表現。
* 以 `ASR` 來說, 當我們欲對`負數`進行`算數右移`卻沒有補入對應個數的 `1` sign bits 時, 這種 `Unspecified_behavior` 就會產生 "錯 (不如預期) 的結果"; 因此, 我們需要對其手動補入對應個數的 `1`。
開始看程式碼 :
1. 我看到 `line 6` 的 `return value`, 判斷 `| (fix ^ (fix >> n)` 為修正操作
2. 繼續往 `(fix ^ (fix >> n)` 來想, 作為修正值, 必須要是 :
* $\overbrace{
\underbrace{11...11}_\text{n bit(s)}
\underbrace{00...00}_\text{32-n bit(s)}
}^\text{32 bits}$ 或是 `0` (不用修正的情況下)。
3. 回推至 `line 4`, 這邊製作出修正值 `fixu`, 已知 `logical` 和 `OP2` 都為 `equality-expression`, 所以 `fixu` 非 `-1` 即 `0`, 這應証了修正值的結果 (如 2. 提到的形式或是 `0`)
4. 考慮我們必須同時滿足 `logical` 和 `OP2` 才需要修正, 我從選項判斷 `OP2` 應為 `m < 0` (即 `m` 為一個負數)
5. 回頭檢視此程式對 "跨平台" 的需求, `OP1` 選擇 `>> 1`
(在不支援 `ASR` 的環境下, 使 `0xffffffff` 做"算數右移"將得到 `0x7fffffff`, 此數在被視為 `signed int type` 的情況下, 大於 `0`; 反之, 支援 `ASR` 的平台仍給我們 `0xffffffff`)
:::warning
line 5 和使用 `int fix = (int) fixu` 有何差別?
:::
### 延伸問題
- [ ] 練習實作其他資料寬度的 ASR,可參照 C 語言:前置處理器應用篇 撰寫通用的巨集以強化程式碼的共用程度;
* char, unsigned long
## 測驗 `2`
在 [C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics) 中,我們介紹過 power of 2 (漢字可寫作「二的冪」)。
> 女星楊冪的名字裡,也有一個「冪」字。且,她取這個名字,就有「次方」的意義,因為她一家三口都姓楊,所以是「楊的三次方」。
[GCC extension](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 其中兩個是 ctz 和 clz:
> Built-in Function: `int __builtin_ctz (unsigned int x)`
> * Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
> Built-in Function: `int __builtin_clz (unsigned int x)`
> * Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
我們可用 `__builtin_ctz` 來實作 LeetCode [342. Power of Four](https://leetcode.com/problems/power-of-four/),假設 int 為 32 位元寬度。實作程式碼如下:
```cpp=
bool isPowerOfFour(int num)
{
return num > 0 && (num & (num - 1))==0 &&
!(__builtin_ctz(num) OPQ);
}
```
### 解題思路
OPQ = ?
* 思考如果一個正整數能被表示成 $4^n$, 其二進位應該是
* ${
\underbrace{00...00}_\text{32 - (2n+1) bits}
\underbrace{1}_\text{1 bit}
\underbrace{00...00}_\text{2n個0}
}$
也就是它能被`算術右移` n 次後得到 `1`
* 觀察 line 3 的 `return` 判斷了三件事情, 分別為 :
1. `num > 0` : `0` 和 `負數` 不可能是 `power of 4`
2. `(num & (num - 1) == 0` : 判斷 `1-bit` 的個數, 在只有一個 `1-bit` 下此條件成立
3. `!(__builtin_ctz(num) OPQ)` : 有了前兩個條件成立, 此條件必須是要判斷 `trailing 0-bits` 是否為偶數個, 因此選擇 `& 0x1`
### 延伸問題
- [ ] 改寫上述程式碼,提供等價功能的不同實作,儘量降低 branch 的數量;
- [x] 練習 LeetCode [1009. Complement of Base 10 Integer](https://leetcode.com/problems/complement-of-base-10-integer/) 和 [41. First Missing Positive](https://leetcode.com/problems/first-missing-positive/),應善用 clz;
- [ ] 研讀 [2017 年修課學生報告](https://hackmd.io/@3xOSPTI6QMGdj6jgMMe08w/Bk-uxCYxz),理解 clz 的實作方式,並舉出 [Exponential Golomb coding](https://en.wikipedia.org/wiki/Exponential-Golomb_coding) 的案例說明,需要有對應的 C 原始程式碼和測試報告;
> [x-compressor](https://hackmd.io/@sysprog/2020-quiz3) 是個以 [Golomb-Rice coding](https://en.wikipedia.org/wiki/Golomb_coding) 為基礎的資料壓縮器,實作中也用到 clz/ctz 指令,可參見 [Selecting the Golomb Parameter in Rice Coding](https://ipnpr.jpl.nasa.gov/progress_report/42-159/159E.pdf)。
## 測驗 `3`
[population count](https://en.wikichip.org/wiki/population_count) 簡稱 popcount 或叫 sideways sum,是計算數值的二進位表示中,有多少位元是 `1`,在一些場合下很有用,例如計算 0-1 稀疏矩陣 (sparse matrix)或 bit array 中非 `0` 元素個數、計算兩個字串的 [Hamming distance](https://en.wikipedia.org/wiki/Hamming_weight)。Intel 在 2008 年 11 月 Nehalem 架構的處理器 Core i7 引入 SSE4.2 指令集,其中就有 `CRC32` 和 `POPCNT` 指令,`POPCNT` 可處理 16-bit, 32-bit, 64-bit 整數。
針對 LeetCode [1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),我們可運用 gcc 內建函式實作,假設 int 寬度為 32 位元,程式碼列表如下:
```cpp=
int numberOfSteps (int num)
{
return num ? AAA + 31 - BBB : 0;
}
```
### 解題思路
AAA = ?
* 根據題目敘述, 我們想知道給定的 `int` type 輸入 `num` 最少可以透過幾個 `steps` 來變成 `0`, 其中:
* 我們透過 `devide by 2` 以及 `subtract 1` 兩種操作使 `num` 逐步變成 `0`
* 以上兩種操作都稱為一個 `step`
* 當我們遇到 `LSB` 為 `1` 的數字(此數字為奇數)時, 我們必須施以 `subtract 1` 的操作, 則我們需要使用 `__builtin_popcount(num)` 次的 `subtract 1` 操作, 可判斷 `AAA` 為`__builtin_popcount(num)`
BBB = ?
* 除去 `1`, 剩下的 `0` 我們可利用 `>>` 操作處理, 然而, 我們需要知道最左邊的 `1` 位於何處
* 因為最後一次的 `subtract 1` 已被算在 `__builtin_popcount(num)` 內, 我們先將 32 減一, 接著透過減掉 `leading zero` 的個數, 可以知道我們需要施以 `>>` 多少次, 因此判斷 `BBB` 為 `__builtin_clz(num)`
### 延伸問題
- [ ] 改寫上述程式碼,提供等價功能的不同實作並解說
> 提示: [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html) 中提及許多 bitwise operation 的使用,如 [bit inverse](https://hackmd.io/@sysprog/ByzoiggIb)、abs 等等,是極好的參考材料。
- [ ] 避免用到編譯器的擴充功能,只用 C99 特徵及 bitwise 運算改寫 LeetCode [1342. Number of Steps to Reduce a Number to Zero](https://leetcode.com/problems/number-of-steps-to-reduce-a-number-to-zero/),實作出 branchless 解法,儘量縮減程式碼行數和分支數量
## 測驗 `4`
考慮以下 64-bit GCD (greatest common divisor, 最大公因數) 求值函式:
```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;
}
```
### 解題思路
XXX = ?
1. 觀察 `line5` 的 `for loop`, 當 `u` 與 `v` 的 `LSB` 同時為 `0` 時, 迴圈將繼續執行, 得知此迴圈計算的 `shift` 為 `u`, `v` 共同的 `tailing zero` 個數
2. 觀察到 `line8`, 的 `while loop`, 在做把 `tailing zero` 全部除去, 這也相當於 "將 `u` 的 `2` 因數全部除去"
3. 因為我們已記錄下共同的 `2` 質因數個數, 接下來在做`輾轉相除法時`就不需考慮 `2`, 而終止條件一樣選 `v`
YYY = ?
* 此動作補回 `tailing zero` 的個數, 因此選 `u << shift`
### 延伸問題
1. 在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升
---
## 測驗 `5`
在影像處理中,[bit array](https://en.wikipedia.org/wiki/Bit_array) (也稱 bitset) 廣泛使用,考慮以下程式碼:
```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;
}
```
可用 clz 改寫為效率更高且等價的程式碼:
```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;
}
```
### 解題思路
KKK = ?
TODO
### 延伸問題
1. 設計實驗,檢驗 clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 [bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html);
2. 思考進一步的改進空間;