# 2022q1 Homework2 (quiz2)
contributed by < [`Kevin-Shih`](https://github.com/Kevin-Shih) >
## 測驗 1 對二個無號整數取平均
`EXP1`:
> 考慮以下對二個無號整數取平均值的程式碼:
> ```c
> #include <stdint.h>
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (a + b) / 2;
> }
> ```
>
> 考慮到整數溢位的因素,以下為第一種可行的實做:
> ```c
> #include <stdint.h>
> uint32_t average(uint32_t low, uint32_t high)
> {
> return low + (high - low) / 2;
> }
> ```
>
> 我們可改寫為以下等價的實作:
> ```c
> #include <stdint.h>
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (a >> 1) + (b >> 1) + (EXP1);
> }
> ```
> 其中 EXP1 為 a, b, 1 (數字) 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
在這個等價實作中可以看到以 `(a >> 1)`, `(b >> 1)` 先將 `a`、`b` 分別除以 2 再相加,避免了整數溢位的可能性,然而當 `a`、`b` 都是奇數時最終的結果會因為兩者在除 2 時各少了 0.5 (右移時均丟失最後 1 bit) 而比正確的平均值少 1 ,因此需要透過加上 `EXP1` 來補償。
有了這些瞭解就可以得出 `EXP1` 只有在 `a`, `b` 都是奇數,也就是 `a`, `b` 最低位均為 1 時為 1 ,其餘為 0 。因此 `EXP1` = `a & b & 1`,其中 `& 1` 用來取最後一個 bit 的結果。
`EXP2`, `EXP3`:
> 再次改寫為以下等價的實作:
> ```c
> uint32_t average(uint32_t a, uint32_t b)
> {
> return (EXP2) + ((EXP3) >> 1);
> }
> ```
> 其中 EXP2 和 EXP3 為 a 和 b 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
看到 return 右半的 `(EXP3) >> 1` 可以把 `EXP3` 除 2,可以看出 `EXP3` 主要是做 `a + b` 的動作,又因題目限用 OR, AND, XOR 這三個運算子,其中只有 XOR 能做到半加器的功能,因此 `EXP3` = `a ^ b`。 接著因半加器缺少進位的動作,所以應該透過加上 `EXP2` 來補償,因進位只發生在 `a`, `b` 均為 1 的 bit 上,故應對 `a`, `b` 做 AND 取得需進位的 bit,且最終求的結果是 $(a + b) / 2$ 因此進位留在原本的 bit 就好不須左移 1 位 (與除 2 時的右移 1 位抵銷),依此推論得到 `EXP2` = `a & b`。
### 比較上述實作在編譯器最佳化開啟時的差異
gcc 版本:
```
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
```
> gcc -S 所得到的 assembly 預設是 AT&T syntax
- ***(a + b) / 2***
以 `gcc -S -O3 q1_1.c` 得到第一種實作經編譯器最佳化後的組合語言輸出如下:
```assembly
// rdi: @a, rsi: @b
average:
.LFB0:
.cfi_startproc
endbr64
leal (%rdi,%rsi), %eax // eax = rdi + rsi
shrl %eax // eax >>= 1
ret
.cfi_endproc
```
第一種實作得到的結果,很直覺的將 `rdi`, `rsi` (`a`, `b`) 中的值相加放入 `eax` 後,將 `eax` 右移 1 位並返回該結果。 從 `rdi`, `rsi` 來看是以 64-bit 來做加法再取 lower 32 bits 存入 `eax`。
- ***low + (high - low) / 2***
以 `gcc -S -O3 q1_2.c` 得到第二種實作經編譯器最佳化後的組合語言輸出如下:
```assembly
// edi: @low, esi: @high
average:
.LFB0:
.cfi_startproc
endbr64
movl %esi, %eax // eax = esi
subl %edi, %eax // eax -= edi
shrl %eax // eax >>= 1
addl %edi, %eax // eax += edi
ret
.cfi_endproc
```
相較於第一種實做,全程採用 32-bit 方式存取暫存器,不像第一種以 64-bit 來做加法再取 lower 32 bits 可能導致整數溢位的問題。
- ***(a >> 1) + (b >> 1) + (a & b & 1)***
以 `gcc -S -O3 q1_3.c` 得到第三種實作經編譯器最佳化後的組合語言輸出如下:
```assembly=
// edi: @a, esi: @b
average:
.LFB0:
.cfi_startproc
endbr64
movl %edi, %eax // eax = edi
movl %esi, %edx // edx = esi
andl %esi, %edi // edi &= esi (a = a & b)
shrl %eax // eax >>= 1
shrl %edx // edx >>= 1
andl $1, %edi // edi &=1
addl %edx, %eax // eax += edx
addl %edi, %eax // eax += edi
ret
.cfi_endproc
```
基本上是以 `eax`, `edx`, `edi` 分別來做 `(a >> 1)`, `(b >> 1)`, `(a & b & 1)` ,但有個疑問是為何 #7 要將 `esi` 移到 `edx` , #12 直接 `addl %esi, %eax` 應會得到相同的結果。
:::warning
TODO
釐清上述疑問
:::
雖改用 `Shift`, `AND` 操作但最佳化後的組合語言輸出反而比前一種多了 4 行,似乎更耗時了(多了 4 個指令),有些出乎意料。
- ***(a & b) + ((a ^ b) >> 1)***
以 `gcc -S -O3 q1_4.c` 得到第四種實作經編譯器最佳化後的組合語言輸出如下:
```assembly
// edi: @a, esi: @b
average:
.LFB0:
.cfi_startproc
endbr64
movl %edi, %eax // eax = edi
andl %esi, %edi // edi &= esi
xorl %esi, %eax // eax ^= esi
shrl %eax // eax >>= 1
addl %edi, %eax // eax += edi
ret
.cfi_endproc
```
以 `eax`, `edi`分別來做 `a ^ b`, `(a ^ b) >> 1` ,最後在相加至 `eax` 並返回該值。
### Linux 核心原始程式碼 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 的 EWMA 實作及 Linux 核心中的應用
EWMA 即 Exponentially weighted moving average ,計算移動平均時較舊的觀測量(數值)權值以指數形式遞減,但永遠不會到 0 ,其函數定義如下:
$S_t = \begin{cases}
Y_0, &t = 0\\
\alpha Y_t + (1-\alpha)\cdot S_{t-1}, &t > 0
\end{cases}$
> - $Y_{t}$ is the value at a time period $t$.
> - $S_{t}$ is the value of the EWMA at any time period $t$. -- Wiki: [EWMA](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
接著看到 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 中定義的 macro
`#define DECLARE_EWMA(name, _precision, _weight_rcp)`, 其三個參數的用途如下
- `name`: 定義一個對應的 struct ,名為 `ewma_##name##` ,以及對應的名稱的 `init`, `read`, `add` 函式
- `_precision`: 計算時 fraction part 要使用多少 bit ,不能大於 30
- `_weight_rcp`: 相當於公式中的 $1/\alpha$ ,為了效率考量須為 2 的冪次(可以用 shift 搞定)
DECLARE_EWMA 所定義的 struct
```c
struct ewma_##name {
unsigned long internal;
};
```
其實只有一個 unsigned long 變數 `internal` 用於儲存 EWMA 的結果。
接著看到第一個函式 `ewma_##name##_init`
```c
static inline void ewma_##name##_init(struct ewma_##name *e)
{
BUILD_BUG_ON(!__builtin_constant_p(_precision));
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp));
/*
* Even if you want to feed it just 0/1 you should have
* some bits for the non-fractional part...
*/
BUILD_BUG_ON((_precision) > 30);
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp);
e->internal = 0;
}
```
前 4 行的 `BUILD_BUG_ON` 均是當輸入的參數不符合要求時可以在 compile 階段就報錯,以保證執行階段時的效率與正確性。 最後就是將 `internal` 初始化為 0 ,計算 EWMA 前應先呼叫此函式以確保 `internal` 初始化為 0 。
> `__builtin_constant_p(x)` 用於檢驗是 `x` 否在 compile time 就已知為常數,若否則中斷 compile 並報錯。 -- [gcc onlinedocs](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
> `BUILD_BUG_ON(condition)` break compile if a condition is true. -- [include/linux/build_bug.h](https://github.com/torvalds/linux/blob/master/include/linux/build_bug.h)
第二個函式 `ewma_##name##_read`
```c
static inline unsigned long ewma_##name##_read(struct ewma_##name *e)
{
BUILD_BUG_ON(!__builtin_constant_p(_precision));
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp));
BUILD_BUG_ON((_precision) > 30);
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp);
return e->internal >> (_precision);
}
```
再度出現同樣 4 行的 `BUILD_BUG_ON` 有點怪,似乎寫一遍即可。
:::warning
TODO
find out why
:::
讀出 `e->internal` 並右移 `_precision` 位,已得到整數位的 EWMA (因 `internal` 保留後 `_precision` bit 作為運算時的 fraction part )。
第三個函式 `ewma_##name##_add`
```c=
static inline void ewma_##name##_add(struct ewma_##name *e,
unsigned long val)
{
unsigned long internal = READ_ONCE(e->internal);
unsigned long weight_rcp = ilog2(_weight_rcp);
unsigned long precision = _precision;
BUILD_BUG_ON(!__builtin_constant_p(_precision));
BUILD_BUG_ON(!__builtin_constant_p(_weight_rcp));
BUILD_BUG_ON((_precision) > 30);
BUILD_BUG_ON_NOT_POWER_OF_2(_weight_rcp);
WRITE_ONCE(e->internal, internal ?
(((internal << weight_rcp) - internal) +
(val << precision)) >> weight_rcp :
(val << precision));
}
```
計算 EWMA 的主體,用於計算新增一筆資料 `val` 後的新 EWMA 並存入 `e->internal` 中。
- #4, #13 使用了 `READ_ONCE`, `WRITE_ONCE` 應該是為了保護在多個 thread 的讀寫操做產生 data race 。
- #5 以 `ilog2(_weight_rcp)` 求得稍後要位移 `weight_rcp` 個 bit 。
- #6 先將 `_precision` 的值存入 `precision` 主要是避免 macro 展開後可能出現的 double evaluation 的情形。
#13 當 `internal` 為 0 時, `e->internal` = `val << precision` 符合 EWMA 定義中 $t = 0$ 的情形。 而當 `internal` 不為 0 時,其敘述相當於 $internal = (internal (2^{weight\_rcp}-1)+val)/2^{weight\_rcp}$
> $2^{weight\_rcp}$ = `_weight_rcp`, `1/_weight_rcp` = $\alpha$
符合 EWMA 定義中 $t > 0$ 的情形。
**EWMA 在 Linux 核心中的應用**
直接查找 `DECLARE_EWMA` 關鍵字在 kernel 中出現的地方,之前修「通訊網路」時在計算 RTT 時出現過 EWMA ,預期 Linux 核心中也會有類似的應用。
Linux 核心中的應用:
- [net/mac80211/ieee80211_i.h](), line 435
- [net/mac80211/sta_info.h](https://github.com/torvalds/linux/blob/master/net/mac80211/sta_info.h), line 126, 373, 374, 433
- 還有不少也是在 drivers/net/wireless 底下的應用...
在 sta_info.h 中 EWMA 被用於計算 TX bit rate(傳輸的 bit rate), failed MSDUs(link layer 的 payload) 的百分比等用途,。
## 測驗 2 不需要分支的 max
`EXP4`, `EXP5`:
> 考慮以下「不需分支的 max」實作:
> ```c
> #include <stdint.h>
> uint32_t max(uint32_t a, uint32_t b)
> {
> return a ^ ((EXP4) & -(EXP5));
> }
> ```
> 其中 EXP4 為 a 和 b 進行某種特別 bitwise 操作,限定用 OR, AND, XOR 這三個運算子。
> EXP5 為 a 和 b 的比較操作,亦即 logical operator,限定用 >, <, ==, >=, <=, != 這 6 個運算子。
>
題目為了取最大值,因此當 `a` 較大的時候回傳值應等價於 `a ^ 0` = `a`。
而當 `b` 較大時回傳值應等價於 `a ^ (a ^ b)` = `0 ^ b` = `b`
因此得出 `EXP4` 應為 `a ^ b`,且 `-(EXP5)` 在 a > b 時為 0,在 a < b 時為 -1 即 0xFFFFFFFF,其中 0xFFFFFFFF 是透過 `-` 對 1 取 2 的補數來的,因此 `EXP5` 在 a > b 時應為 0,在 a < b 時應為 1,故 `EXP5` = `a < b`。
### 針對 32 位元有號整數,撰寫同樣 branchless 的實作
```c
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
將輸入的 parameters 改為 `int32_t` 型態就能以相同的方式的到正確的 max 結果了。
### 在 Linux 核心原始程式碼中,找出更多類似的實作手法
[arch/x86/kvm/mmu.h](https://github.com/torvalds/linux/blob/master/arch/x86/kvm/mmu.h) 中:
```c
/*
* If CPL < 3, SMAP prevention are disabled if EFLAGS.AC = 1.
*
* If CPL = 3, SMAP applies to all supervisor-mode data accesses
* (these are implicit supervisor accesses) regardless of the value
* of EFLAGS.AC.
*
* This computes (cpl < 3) && (rflags & X86_EFLAGS_AC), leaving
* the result in X86_EFLAGS_AC. We then insert it in place of
* the PFERR_RSVD_MASK bit; this bit will always be zero in pfec,
* but it will be one in index if SMAP checks are being overridden.
* It is important to keep this branchless.
*/
unsigned long smap = (cpl - 3) & (rflags & X86_EFLAGS_AC);
int index = (pfec >> 1) +
(smap >> (X86_EFLAGS_AC_BIT - PFERR_RSVD_BIT + 1));
```
git log 中還能找到一些其他的實做,但有些似乎已經消失了,在目前 github 上的 repo 找不到了
- [Replace int2float() with an optimized version.](https://github.com/torvalds/linux/commit/747f49ba67b8895a5831ab539de551b916f3738c)
- [Optimize pte permission checks](https://github.com/torvalds/linux/commit/97d64b788114be1c4dc4bfe7a8ba2bf9643fe6af)
## 測驗 3 64 位元 GCD 求值函式
> 考慮以下 64 位元 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 (COND);
> return RET;
> }
> ```
根據題目中提到的 GCD 演算法,當 `u`, `v` 任一者為 0 時, gcd 即為非零者的值,當均為 0 時,gcd 為 0 。 這三種情形透過 #4 的 if 敘述來處理。
接著 #6 的 for 迴圈會在 `u`, `v` 均為偶數時,將其同除以 2 ,並將除的次數紀錄在 `shift` 以便最後乘回 gcd 中(左移 `shift` 次)。 #9 的 while 迴圈則是當 `u` 還是偶數時,除 2 直到 `u` 變為奇數。
接著 #11 開始的 do while 就是輾轉相除法的本體了,當 `u` 小於 `v` 就將 `v` 減去 `u` ,大於的話就將 `u - v` 存到 `v` , `v` 存到 `u` ,直到其中一者為 0 為止,然而由於這個實做中,相減後的結果都會存到 `v` ,因此中止條件 `COND` 只須判斷 `v` 是否為 0 即可,故 `COND` = `v` 。
最終的回傳值 `RET` 則是將先前輾轉相除法最後留下的 `u` 值左移 `shift` 位(先前同除以 2 的次數)得到答案,因此 `RET` = `u << shift` 。
### 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
**透過 __builtin_ctz 改寫的 gcd64_ctz**
```c
uint64_t gcd64_ctz(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift = __builtin_ctz(u | v);
u >>= __builtin_ctz(u);
do {
v >>= __builtin_ctz(v);
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
將原先同除以 2 的部份改為紀錄 `u`, `v` 中最低位的 1 作為 `shift` 。
```c
while (!(u & 1))
u /= 2;
```
取代為 `u` 直接右移到無法被 2 整除。 `v` 的部份則留到迴圈內處理。
其餘部份與原先的 `gcd64` 相同。
**效能測試方式**
先以 random 方式準備測試資料
```c
void prepare_data(uint64_t *data1, uint64_t *data2, size_t size){
srand(time(NULL));
for (size_t i = 0; i < size; i++) {
data1[i] = rand();
data2[i] = rand();
}
}
```
兩者分別測試
```c
prepare_data(data1, data2, size);
clock_t start = clock();
for (size_t i = 0; i < size; i++)
gcd64(data1[i], data2[i]);
clock_t finish = clock();
clock_t start2 = clock();
for (size_t i = 0; i < size; i++)
gcd64_ctz(data1[i], data2[i]);
clock_t finish2 = clock();
double time1 = (double)(finish - start) / CLOCKS_PER_SEC;
double time2 = (double)(finish2 - start2) / CLOCKS_PER_SEC;
```
在本機連續執行 10,000,000 次、 3 round 的結果如下
(5 round 取中間 3 round)
||gcd64|gcd64_ctz|
|-|-|-|
| 1 | 2.125378 secs | 0.936948 secs |
| 2 | 2.126757 secs | 0.936914 secs |
| 3 | 2.121405 secs | 0.935645 secs |
| avg.| 2.124513 secs | 0.936502 secs |
採用 ctz 的實做 3 round 的平均快了約 2.268562 倍,顯示在測試環境中 `ctz` 顯然比除法運算快上不少。
### 請解釋 Linux 核心中內建 GCD (不只一種實作),的實作手法並探討在核心內的應用場景。
**[lib/math/gcd.c](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c) 中的 GCD 實作手法**
```c
unsigned long gcd(unsigned long a, unsigned long b)
{
unsigned long r = a | b;
if (!a || !b)
return r;
b >>= __ffs(b);
if (b == 1)
return r & -r;
for (;;) {
a >>= __ffs(a);
if (a == 1)
return r & -r;
if (a == b)
return a << __ffs(r);
if (a < b)
swap(a, b);
a -= b;
}
}
```
當 ffs 可用時會採用以上實做,當 `a`, `b` 中一者為 0 或均為 0 時,gcd 為 `a | b` (三種情形分別對應 `b`, `a`, `0`)。
若 `b` 是 2 的冪次,則回傳 `r & -r`。 `r & -r` 的作法是透過 2 補數特性找出 `r` 中最低位的 1 並使其他 bit 為 0 (就像測驗 4 中 `bitset & -bitset`),而由於 `r` = `a | b` 因此相當於找出 `min(a & -a, b & -b)` ,也就是兩者的最大公因數(因 `b` 是 2 的冪次,兩者 gcd 是小於等於 `b` 的 2 的冪次)。
若 `b` 不是 2 的冪次, `b >>= __ffs(b)` 也會如 gcd 算法中所述,將 `b` 除到剩下奇數的狀態。
for 迴圈內有 4 步:
- 將 `a` 除到剩下奇數的狀態。
- 若 `a` 是 2 的冪次,若是則 gcd 為 `r & -r` (同 `b` 的情形,代表沒有 2 的冪次以外的公因數)。
- 若剩下的 `a == b` 代表剩下的 `a` 也是兩者的公因數,將 `a` 左移先前同除的 2 的次數(`ffs(r)` = `min(ffs(a), ffs(b))`)即為 gcd 。 (再減一次就是輾轉相除法,其中一個減到變 0 的情形)
- 最後當上述情形都不成立時,執行輾轉相除法中相減的步驟,若 a < b 就先交換 `a`, `b`,確保 `a` 是被減的那個。
**核心內的應用場景**
- [block/blk-settings.c](https://github.com/torvalds/linux/blob/master/block/blk-settings.c)
- [drivers/media/tuners/mt2063.c](https://github.com/torvalds/linux/blob/master/drivers/media/tuners/mt2063.c)
- [sound/core/pcm_timer.c](https://github.com/torvalds/linux/blob/master/sound/core/pcm_timer.c)
- ...
其中在 `mt2063.c` 中一個用於檢測 [LO](https://en.wikipedia.org/wiki/Local_oscillator) spur 是否發生的函數 `IsSpurInBand()` 中出現,用於計算某種 scale factor 。
```c
/*
** For each edge (d, c & f), calculate a scale, based on the gcd
** of f_LO1, f_LO2 and the edge value. Use the larger of this
** gcd-based scale factor or f_Scale.
*/
lo_gcd = gcd(f_LO1, f_LO2);
gd_Scale = max((u32) gcd(lo_gcd, d), f_Scale);
hgds = gd_Scale / 2;
gc_Scale = max((u32) gcd(lo_gcd, c), f_Scale);
hgcs = gc_Scale / 2;
gf_Scale = max((u32) gcd(lo_gcd, f), f_Scale);
hgfs = gf_Scale / 2;
```
另外在 `git log --grep` 則發現不少 commit 是捨棄 local 的 gcd implementation 而改用 [lib/math/gcd.c](https://github.com/torvalds/linux/blob/master/lib/math/gcd.c) 的實作,例如剛剛提到的 sound/core/pcm_timer.c 在 [commit a96053](https://github.com/torvalds/linux/commit/a9605391cfab2bf9a73e51faac5147622f73c6d5) 時捨棄。
## 測驗 4 bitset
> 在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
> ```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;
> }
> 以 __builtin_ctzll 改寫的程式碼如下:
> ```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]; // a row of bitmap per iteration
> while (bitset != 0) { // till this row has no more `set bit`
> uint64_t t = EXP6;
> int r = __builtin_ctzll(bitset);
> out[pos++] = k * 64 + r;
> bitset ^= t;
> }
> }
> return pos;
> }
> ```
> 其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 $000101_b$,那 `t` 就會變成 $000001_b$,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
題目提到 `EXP6` 應保留 `bitset` 中最低位的 1 而其餘均為 0 ,又提示要 `-bitset` 的特性,以 bitset 為 $010100_b$ 為例, `-bitset` 為 $101100_b$ ,兩者只有原先最低位的 1 的 bit 均為 1 ,因此善用 2 的補數特性, `EXP6` = `bitset & -bitset`。
`-bitset` 特性:
| | binary |
|---------|------------|
| bitset | $010100_b$ |
| ~bitset | $101011_b$ |
| -bitset | $101100_b$ |
`-bitset` = `~bitset + 1` 因此只有最低位的 1 與 bitset 均為 1 。
### 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density
**準備測試資料的函式**
```c
void prepare_data(uint64_t *bitmap, size_t size, int density){
srand(time(NULL));
// control bitmap density with 0 ~ 4 level 0%, 25%, 50%, 75%, 100%
int count = 16 * size * density;
// 100% density
if (density == 4){
for (size_t i = 0; i < size; i++)
bitmap[i] = 0xFFFFFFFFFFFFFFFFul;
return;
}
// init bitmap
for (size_t i = 0; i < size; i++)
bitmap[i] = 0;
// 0% density
if (density == 0)
return;
// select `count` (pseudo) random `0` bits set them to `1`
while (count) {
int i = rand64() % (size * 64);
if (bitmap[i / 64] & 1ul << (i % 64))
continue;
else{
bitmap[i / 64] |= 1ul << (i % 64);
count--;
}
}
}
```
**測試方式**
```c
size_t bitmapsize = strtoul(argv[1], NULL, 10);
uint64_t *bitmap = (uint64_t *)malloc(bitmapsize * sizeof(uint64_t));
uint32_t *out_naive = (uint32_t *)malloc(bitmapsize * 64 * sizeof(uint32_t));
uint32_t *out_improved = (uint32_t *)malloc(bitmapsize * 64 * sizeof(uint32_t));
for(size_t i = 0; i < 5; i++){ // 5 density level 0%, 25%, 50%, 75%, 100%
prepare_data(bitmap, bitmapsize, i);
for (size_t j = 0; j < bitmapsize * 64; j++)
out_naive[j] = out_improved[j] = 0;
clock_t start = clock();
naive(bitmap, bitmapsize, out_naive);
clock_t finish = clock();
clock_t start2 = clock();
improved(bitmap, bitmapsize, out_improved);
clock_t finish2 = clock();
double time1 = (double)(finish - start) / CLOCKS_PER_SEC;
double time2 = (double)(finish2 - start2) / CLOCKS_PER_SEC;
printf("density: %3ld%%, naive: %f secs, improved: %f secs\n", i * 25, time1, time2);
}
free(bitmap);
free(out_naive);
free(out_improved);
```
測試 5 種不同的 bitmap density 的情形下 `naive` 與 `ctz` 改寫的版本的執行時間。
在我的環境中執行 `bitmapsize` = 1,000,000 的結果:
| density | naive (secs) | improved (secs) |
|-|-|-|
| 0% | 0.129309 | 0.002487 |
| 25% | 0.276713 | 0.069022 |
| 50% | 0.390655 | 0.121114 |
| 75% | 0.273711 | 0.174631 |
| 100% | 0.144110 | 0.229330 |
![](https://i.imgur.com/hp5QZq4.png)
在絕大多數時間 `ctz` 改寫的 `improved` 版本確實比 `naive` 快上不少,但有趣的是在 100% density (all set) 的情形中 `naive` 反而比 `improved` 快上一些。
兩個版本的主要差異:
**naive**
```c
for (int i = 0; i < 64; i++) {
if ((bitset >> i) & 0x1)
out[pos++] = p + i;
}
```
**improved**
```c
while (bitset != 0) { // till this row has no more `set bit`
uint64_t t = EXP6;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
```
在 100% density 兩者的內層迴圈都須 iterate 64 次且 `naive` 的 if 條件恆為真,因此 prediction miss 幾乎不會出現,因此迴圈內單純對 `out[pos++]` 賦值的 `naive` 反而比較快。
另外, `naive` 版本在 50% density 時的執行時間最久,應是因為 prediction miss 較高, `improved` 版本則是隨著 density 上升執行時間隨之遞增 (`ctz` 所能省下內層迴圈 iteration 次數越來越少)。
### 思考進一步的改進空間
```c=
size_t improved_v2(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) {
uint64_t t = bitset & -bitset;
out[pos++] = k << 6 | __builtin_ctzll(bitset);
bitset ^= t;
}
}
return pos;
}
```
#9 中避免使用 `*`, `+` 運算改用 `<<`, `|` ,並將原本 `__builtin_ctzll(bitset)` 不再存入 `r` 而直接移至 #9 使用讓程式更簡潔。
:::spoiler **測試程式**
```c
int main(int argc,char **argv){
if (argc != 2)
return 1;
size_t bitmapsize = strtoul(argv[1], NULL, 10);
uint64_t *bitmap = (uint64_t *)malloc(bitmapsize * sizeof(uint64_t));
uint32_t *out_naive = (uint32_t *)malloc(bitmapsize * 64 * sizeof(uint32_t));
uint32_t *out_improved = (uint32_t *)malloc(bitmapsize * 64 * sizeof(uint32_t));
uint32_t *out_improved_v2 = (uint32_t *)malloc(bitmapsize * 64 * sizeof(uint32_t));
for(size_t i = 0; i < 5; i++){ // 5 density level 0%, 25%, 50%, 75%, 100%
prepare_data(bitmap, bitmapsize, i);
for (size_t j = 0; j < bitmapsize * 64; j++)
out_naive[j] = out_improved[j] = out_improved_v2[j] = 0;
clock_t start = clock();
naive(bitmap, bitmapsize, out_naive);
clock_t finish = clock();
clock_t start2 = clock();
improved(bitmap, bitmapsize, out_improved);
clock_t finish2 = clock();
clock_t start3 = clock();
improved_v2(bitmap, bitmapsize, out_improved_v2);
clock_t finish3 = clock();
// test output correctness
int equal = 1;
for (int i = 0; i < bitmapsize * 64; i++){
if (out_naive[i] != out_improved[i]) {
printf("improved failed\n");
equal = 0;
break;
}
if (out_naive[i] != out_improved_v2[i]) {
printf("improved_v2 failed\n");
equal = 0;
break;
}
}
double time1 = (double)(finish - start) / CLOCKS_PER_SEC;
double time2 = (double)(finish2 - start2) / CLOCKS_PER_SEC;
double time3 = (double)(finish3 - start3) / CLOCKS_PER_SEC;
printf("density: %3ld%%, naive: %f secs, improved: %f secs, improved_v2: %f secs\n", i * 25, time1, time2, time3);
}
free(bitmap);
free(out_naive);
free(out_improved);
free(out_improved_v2);
return 0;
}
```
:::
(與前面測試 `ctz` 版本的方式相同,但加入檢查 output 正確性的程式,避免 `improved_v2` 改出 bug )
在我的環境中執行 `bitmapsize` = 1,000,000 的結果如下
| density | naive (secs) | improved (secs) | improved_v2 (secs) |
|-|-|-|-|
| 0% | 0.135698 | 0.002625 | 0.002510 |
| 25% | 0.291539 | 0.071203 | 0.067933 |
| 50% | 0.395848 | 0.121448 | 0.117784 |
| 75% | 0.274069 | 0.173835 | 0.169411 |
| 100% | 0.143882 | 0.213872 | 0.206336 |
**聚焦在 `improved`, `improved_v2` 的折線圖**
![](https://i.imgur.com/uXS5mg8.png)
從折線圖可以看到避免使用 `*`, `+` 而改用 bitwise operator `<<`, `|` 可以節省一小部份的執行時間,而且在 density 較高時比較明顯。
### 閱讀 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) 並舉出 Linux 核心使用 bitmap 的案例
- drivers/net/ipa/ipa_endpoint.h : [commit c1aaa0](https://github.com/torvalds/linux/commit/c1aaa01dbf4cef95af3e04a5a43986c290e06ea3)
- net/mptcp/pm_netlink.c : [commit 86e39e](https://github.com/torvalds/linux/commit/86e39e04482b0aadf3ee3ed5fcf2d63816559d36)
- [sound/usb/clock.c #260](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/sound/usb/clock.c#L260)
- ...
大多數是用來儲存一種狀態如 flags, visted 等等,相較使用 bool 佔用 1 byte 儲存 1 個狀態 (true / false), bitmap 1 byte 最多儲存 8 個狀態,若是需要節省空間, bitmap 是很好的選擇。
> [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 中 bitmap 是以 unsigned long array 方式宣告的,因此若要存 65 個狀態就需要 128 bits 似乎也是會浪費不少空間 (但同時也保留了不少未來擴充更多狀態的空間)