---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `BensonYang1999` >
> [第 2 周測驗題](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗 1:兩數取平均
### 解題
由於兩個無號整數相加時有可能會發生溢位,因此考慮使用以下不同方法
1. 已知兩數大小,可直接使用利用以下方式
```c
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
2. 利用位移運算子可避免使用方法1中的除法,且不需預先知道兩數的大小
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
```
其中的 `a & b & 1` 是用來補償小數進位的部份,原理為判斷兩數是否都是奇數
以下面的例子為例
`a=5, b=7 => a>>1=2, b>>1=3 => added=5`
因此需要補加上1,才會得到最終的數值6
3. 利用 bit-wise operator 實作加法器
在[你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)有提到利用 bit-wise operator 實作加法器 `x + y = x ^ y + ( x & y ) << 1`
由加法式可以進階推導出 `(x + y) / 2 = (x & y) + ((x ^ y) >> 1`
因此可以利用上式實作兩數平均
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
:::info
延伸問題:
- [x] 解釋上述程式碼運作的原理
- [x] 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
- [ ] 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
> 移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
### 編譯器最佳化後的組合語言比較
> 利用 gcc 中的 `-S` 指令可以輸出組合語言,額外加上 `-O3` 可確保編譯啟開啟最高等級的最佳化
方法1
```c
#include <stdio.h>
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
int main()
{
printf("%u\n", average(5, 7));
}
```
方法2
```c
#include <stdio.h>
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
int main()
{
printf("%u\n", average(5, 7));
}
```
方法3
```c
#include <stdio.h>
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
int main()
{
printf("%u\n", average(5, 7));
}
```
利用diff比較方法1與方法2的差別
```shell
$ diff test1.s test2.s
10,11c10,12
< movl %esi, %eax
< subl %edi, %eax
---
> movl %edi, %eax
> movl %esi, %edx
> andl %esi, %edi
12a14,16
> shrl %edx
> andl $1, %edi
> addl %edx, %eax
```
* 雖然方法1的組合語言較精簡,但會用到 subl 除法運算,因此可能會花很多 clock cycles 去執行運算
* 這兩個方法的比較並不恰當,因方法1需額外加入比較兩個數字大小比較的判斷式,會需要額外的指令
利用diff比較方法2與方法3的差別
```shell
$ diff test2.s test3.s
11d10
< movl %esi, %edx
12a12
> xorl %esi, %eax
14,16d13
< shrl %edx
< andl $1, %edi
< addl %edx, %eax
```
* 方法3使用 xor 取代移動、位移、相加、 and 指令,因此判斷可以節省執行時間
:::warning
使用 `diff -u` 命令,更好觀察。另外,方法 `1` 應該補充數值範圍的檢查程式碼。
:notes: jserv
:::
---
## 測驗 2:bitwise max
### 解題
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
考慮 `a < b` 及 `a >= b` 兩種情況
1. 當 `a < b`
& 右邊會等於 0xFFFFFFFF ,即可以省略 `& -(a < b)` 這部份
剩下的部份 `a ^ (a ^ b)` 可進一部簡化為 `b`
因此最後的算式 => `return b;`
2. 當 `a >= b`
& 右邊會等於 0 ,代表 `((a ^ b) & 0)` 會等於0
因此最後的算式 => `return a;`
:::info
延伸問題:
- [x] 解釋上述程式碼運作的原理
- [x] 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
- [ ] Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c:
```c
/*
* Logically, we're doing "if (i & lsbit) i -= size;", but since the
* branch is unpredictable, it's done with a bit of clever branch-free
* code instead.
*/
__attribute_const__ __always_inline
static size_t parent(size_t i, unsigned int lsbit, size_t size)
{
i -= size;
i -= size & -(i & lsbit);
return i / 2;
}
```
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
:::
### min/max branchless 實作
討論無號整數的 `min`,可以透過簡單修改 `max` 的程式碼達成
```c
#include <stdint.h>
uint32_t min(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a > b));
}
```
有號整數的部份,由於此運算方式可以適用於有號數,因此只需修改變數型態即可
```c
#include <stdint.h>
int32_t max(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
int32_t min(int32_t a, int32_t b)
{
return a ^ ((a ^ b) & -(a > b));
}
```
---
## 測驗 3:GCD
### 解題
參考本題的註解
```c=
#include <stdint.h>
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v; // no.1
int shift; // 紀錄共除2的幾次方
for (shift = 0; !((u | v) & 1); shift++) {
u /= 2, v /= 2;
} // no.3
while (!(u & 1))
u /= 2; // no.4
do {
while (!(v & 1))
v /= 2; // no.5
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (COND);
return RET;
}
```
從第14行開始沒有在註解中說明,查閱網路資料後可得知其為 [Euclid's algorithm](https://en.wikipedia.org/wiki/Greatest_common_divisor) ,其結束條件為兩數相等,因此判斷 `COND` 的空格應該填入 `v` ,也就是被減數不等於0時
而最後RET的地方需要把一開始同除的2次方給補回去,因此應該是 `u << shift`
:::info
延伸問題:
- [x] 解釋上述程式運作原理;
- [x] 在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升;
- [ ] Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
:::
### __builtin_ctz 改寫 GCD
查閱 [GNU 說明書](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html),得知本函數可以取得變數中從最低位數有幾的0,也就可以判斷其因數包含2的多少次方,因此可以用來加速原本使用 while 判斷的地方,也就是說程式可以修改為
```c=
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;
}
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;
}
```
仔細觀察後發現,發現第4~8行其實可以整並,改寫為
```c
int shift = __builtin_ctz(u) < __builtin_ctz(v) ? __builtin_ctz(u) : __builtin_ctz(v);
u >>= __builtin_ctz(u);
v >>= __builtin_ctz(v);
```
完整程式碼
```c
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift = __builtin_ctz(u) < __builtin_ctz(v) ? __builtin_ctz(u) : __builtin_ctz(v);
u >>= __builtin_ctz(u);
v >>= __builtin_ctz(v);
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;
}
```
利用傳入大數的方式,讓執行時間變長,可以明顯比較效能差異
我利用 linux 內建指令 `time` 進行計時,並帶入 uint64 的最大值 18446744073709551615 與其 -1 ,結果如下
```shell
original version:
$ time ./a.out
real 0m0.003s
user 0m0.002s
sys 0m0.000s
__builtin_ctzll version:
$ time ./a.out
real 0m0.003s
user 0m0.002s
sys 0m0.000s
```
兩者時間都太短了,因此改變方式,使用計算相同數字多次計時,結果如下
```shell
original version:
$ time ./a.out
real 0m1.372s
user 0m1.372s
sys 0m0.000s
__builtin_ctz version:
$ time ./a.out
real 0m1.100s
user 0m1.100s
sys 0m0.000s
```
大概加快 `25%`
---
## 測驗 4:bit array 使用
### 解題
依照本題的提示,考慮使用 `-bitwise` ,由於再 C 語言裡面負號的計算方式是將所有bits反轉後再加 1 ,因此可以利用此特性,與 `bitwise` 做 & 運算,及可以達成找到最低的 1 的位置,即本題的要求
```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 = bitset & -bitset;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
:::info
延伸問題:
- [ ] 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
- [ ] 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
- [ ] 思考進一步的改進空間;
- [ ] 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例;
:::