---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `Xx-oX` >
> [題目](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗 `1`
考慮對兩個無號整數取平均值的程式碼
```c
#include <stdint.h>
uint32_t average(uint32_t low, uint32_t high)
{
return low + (high - low) / 2;
}
```
可以改寫成以下等價的實作
* `A`
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (EXP1);
}
```
或者
* `B`
```c
uint32_t average(uint32_t a, uint32_t b)
{
return (EXP2) + ((EXP3) >> 1);
}
```
填入 EXPx ,並
* 解釋程式碼運作原理
* 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀
* 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
### `A`
`a >> 1` 表示 $\lfloor\dfrac{a}{2}\rfloor$
而 $\lfloor\dfrac{a}{2}\rfloor + \lfloor\dfrac{b}{2}\rfloor$ 在 $a, b$ 均為奇數時,會比預期的結果少 1
> e.g. a = 11, b = 13, 均為 unsigned (實行邏輯右移)
> ```verilog
> (11 >> 1) + (13 >> 1)
> = (b'1011 >> 1) + (b'1101 >> 1)
> = b'0101 + b'0110
> = b'1011 = 11
> ```
> 比預期的答案 (12) 少一
> 原因是右移會把 LSB 捨去,而兩個奇數 LSB 加總的進位也就被忽略了
因此,EXP1 要判斷 a, b 是否均為奇數,如果成立,則加上被捨去的 1
判斷奇數 => 判斷 LSB => 跟 1 做 `&` (AND) => `a & b & 1`
### `B`
考慮半加器
```c
s = a ^ b // 和 a XOR b
c = a & b //進位 a AND b
```
a 和 b 的平均 = $\lfloor\dfrac{a+b}{2}\rfloor$
比照 `A` 右移 s 而省略的 LSB 以及進位由 c 補上
因此
```c
s = ((a ^ b) >> 1)
c = a & b
```
EXP2 填入 `a & b`
EXP3 填入 `a ^ b`
> 以 a = 11, b = 13 驗證
> ```verilog
> (11 & 13) + ((11 ^ 13) >> 1)
> = (b'1011 & b'1101) + ((b'1011 ^ b'1101) >> 1)
> = b'1001 + (b'0110 >> 1)
> = b'1001 + b'0011 = b'1100 = 12
> ```
### 比較在編譯器最佳化開啟時的組合語言輸出
:::info
環境: gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
命令: gcc -S -O3 t.c
> 之後會嘗試改用 compiler explorer,以圖方便
測試程式 `t.c`
```c
#include <stdint.h>
uint32_t ver1(uint32_t a, uint32_t b)
{
return (a >> 1) + (b >> 1) + (a & b & 1);
}
uint32_t ver2(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
:::
`A` 的對應組合語言
```go
movl %edi, %eax
movl %esi, %edx
andl %esi, %edi ;// a & b
shrl %eax ;// a >> 1
shrl %edx ;// b >> 1
andl $1, %edi ;// (a & b) & 1
addl %edx, %eax ;// (a >> 1)+ (b >> 1)
addl %edi, %eax ;// 加上 (a & b & 1)
ret
```
`B` 的對應組合語言
```go
movl %edi, %eax
andl %esi, %edi ;// a & b
xorl %esi, %eax ;// a ^ b
shrl %eax ;// (a ^ b) >> 1
addl %edi, %eax ;// 相加
ret
```
發現 `B` 版本相比 `A` 版本
* 少了 1 次 `mov` 操作
* 少了 2 次 `and` 操作
* 少了 1 次 `shr` 操作
* 多了 1 次 `xor` 操作
> 註: 後綴 `l` 表示是針對 32-bit 暫存器的指令
> TODO: 以第一手資料解釋
## 測驗 `2`
考慮以下程式碼
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((EXP4) & -(EXP5));
}
```
填入 EXPx ,並
* 解釋上述程式碼運作的原理
* 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
* Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c... 請在 Linux 核心原始程式碼中,找出更多類似的實作手法。(請善用 git log 檢索)
### 運作原理
> 參考用真假值表
> | $\oplus$ | 0 | 1 | | $\wedge$ | 0 | 1 |
> | --- | --- | --- | --- | --- | --- | --- |
> | 0 | 0 | 1 | | 0 | 0 | 0 |
> | 1 | 1 | 0 | | 1 | 0 | 1 |
先不考慮兩者相等的情形
觀察程式碼,發現回傳 a ^ (...)
根據 [XOR ($\oplus$, `c:^`)](https://en.wikipedia.org/wiki/Exclusive_or) 的特性進行判斷
* $A \oplus 0 = A$ `恆等律`
得知當 $a > b$ 時, `(EXP4 & -(EXP5))` 的值為 0
* $A \oplus B \oplus B = A$ `自反律`
> 由 $A \oplus A = 0$ `歸零律` 且 $(A \oplus B) \oplus C = A \oplus (B \oplus C)$ `結合律` 得知
得知當 $a < b$ 時, `(EXP4 & -(EXP5))` 的值為 `a ^ b`
> `a ^ a ^ b = b`
再觀察 `(EXP4 & -(EXP5))`
根據 [AND ($\wedge$, `c:&`)](https://en.wikipedia.org/wiki/Logical_conjunction) 的特性
* 任意 bit $a \wedge 1 = a$ `保真性`
=> `A & 0xFFFFFFFF = A` 其中 `0xFFFFFFFF` 為 -1 (2's complement)
* $A \wedge 0 = 0$ `保假性`
得知應控制 EXP5 的值,使其在 $a < b$ 時為 1, $a > b$ 時為 0
而 EXP4 則填入 `a ^ b` 使得 $a < b$ 時 `a ^ (...) = a ^ a ^ b = b`
綜上所述
* EXP4 應填入 `a ^ b`
* EXP5 應填入 `a < b` 或 `a <= b` (a, b 相等時回傳何者皆可)
使得結果 `a ^ ((a ^ b) & -(a < b))` 等於 `max(a, b)`
### 針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
```c
int max(int a, int b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
無號數的情形即為題目的情形 (`uint32_t`)
而有號數也可用相同的方法求最大值
驗證如下
> 取 a = -5, b = 3
> ```verilog
> a = -5 = b'1011
> b = 3 = b'0011
> a ^ b = b'0111
> a < b = b'0001 (true)
> -b'0001 = b'1111
> b'0111 & b'1111 = b'0111
> b'1011 ^ b'0111 = b'0011 = 3
> ```
> 取 a = -5, b = -3
> ```verilog
> a = -3 = b'1011
> b = -5 = b'1101
> a ^ b = b'0110
> a < b = b'0001 (true)
> -b'0001 = b'1111
> b'0110 & b'1111 = b'0110
> b'1101 ^ b'0110 = b'1011 = -3
> ```
因此使用相同的程式碼即可對有/無號數作找最大值的運算
## 測驗 `3`
考慮以下 64 位元 GCD 求值函式
```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;
}
```
補完上述程式碼,並
* 解釋上述程式運作原理
* 在 x86_64 上透過 `__builtin_ctz` 改寫 GCD,分析對效能的提升
* Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
### 運作原理
> 註: GCD 演算法
> 以測驗題目提供的演算法為基底,並補上兩者皆為奇數的情形,參考自 [Binary GCD algorithm](https://en.wikipedia.org/wiki/Binary_GCD_algorithm)
> ```pascal
> function GCD(x, y: integer): integer;
> begin
> if x = y and x = 0 then result := 0;
> if y = 0 then result := x;
> if x = 0 then result := y;
> (*because everything divides 0*)
> if x mod 2 = 0 and y mod 2 = 0 then result := 2 * GCD(x/2, y/2);
> (*because 2 is a common divisor,
> multiplication with 2 can be done with bitwise shift operation*)
> if x mod 2 = 0 and y mod 2 = 1 then result := GCD(x/2, y);
> if x mod 2 = 1 and y mod 2 = 0 then result := GCD(x, y/2);
> (*because 2 is not a common divisor*)
> if x mod 2 = 1 and y mod 2 = 1 then result := GCD(Abs(u - v), Min(u, v));
> (*gcd(u, v) = gcd(|u − v|, min(u, v)), if u and v are both odd.*)
> end;
> ```
參考上述 GCD 演算法
並以 u 紀錄結果,搭配 do-while 迴圈將遞迴去除
第 4 行是判斷 u, v 中是否有一者(或兩者)為 0
> $A \vee 0 = A$
> $0 \vee 0 = 0$
第 6 到第 8 行的 for 迴圈是當 u, v 皆為偶數時,將兩者均除以 2 ,並用 `shift` 紀錄待會要乘以 2 的次數
第 9 到第 10 行的迴圈不斷將 u 除以 2 直到它變成奇數
第 12 到第 13 行的迴圈不斷將 v 除以 2 直到它變成奇數
第 14 到第 20 行則是兩者皆為奇數的情形,相減並對 u - v 以及 u, v 中較小者作 GCD 直到出現 0
因此 21 行的 COND 應填入 `v` 判斷被減數 v 是否不為 0
而 RET 應填入 `u << shift` 將前面沒乘上的 2 通通乘回來
### 透過 `__builtin_ctz` 改寫
> 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.
e.g. 6 = b'0110
__builtin_ctz(6) = 1 -> 最右邊的 1 右邊有 1 個 0
將連續除 2 的動作用 `__builtin_ctz` 替代
> e.g.
> ```verilog
> 連續除以 2
> 4 = b'0100
> 2 = b'0010
> 0 = b'0001
> 使用__builtin_ctz()
> __builtin_ctz(4) = 2
> 4 >> 2 = b'0100 >> 2 = b'0010 = 0
> ```
> 本來還想把判斷 0 的動作也替換,後來發現當它為 0 時行為未定
改寫實作
> 因為是 uint64_t 故使用 __builtin_ctzll (unsigned long long 版本)
```c
uint64_t gcd64(uint64_t u, uint64_t v)
{
if (!u || !v) return u | v;
int shift = (__builtin_ctzll(u) > __builtin_ctzll(v)) ? __builtin_ctzll(v) : __builtin_ctzll(u);
u >>= __builtin_ctzll(u);
v >>= __builtin_ctzll(v);
do {
v >>= __builtin_ctzll(v);
if (u < v) {
v -= u;
} else {
uint64_t t = u - v;
u = v;
v = t;
}
} while (v);
return u << shift;
}
```
## 測驗 `4`
在影像處理中,bit array 被廣泛使用,如
```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];
while (bitset != 0) {
uint64_t t = EXP6;
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= t;
}
}
return pos;
}
```
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 `b'000101`,那 t 就會變成 `b'000001`,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
填入 EXP6 ,並
* 解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中
* 設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density
* 思考進一步的改進空間
* 閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例
### 填空
3 bits 時二補數的編碼
| | 1 | 2 | 3 |
| - | --- | --- | --- |
| + | 001 | 010 | 011 |
| - | 111 | 110 | 101 |
|+&-| 001 | 010 | 001 |
可以發現將 $A \wedge (\sim A + 1)$ (`c: A & (-A)`) 會得到最低位元的 1
> 以 bitset = 20 = `b'00010100` 為例
> -bitset = ~bitset + 1 = `b'11101011` + `1` = `11101100`
> bitset & -bitset = `b'00000100` 即為所求
其中 unsigned 與 signed 僅為判讀上的差異,實際上存的 bitmap 皆相同
在 bitwise 操作時使用 unsigned 以獲得更大的可攜性 [你所不知道的 c 語言 - bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)
> printf(...) 中 %d 會以 signed 的方式判讀,而 %u 會以 unsigned 的方式判讀
因此 EXP6 應填入 `bitset & -bitset`
## 測驗 `5`
考慮以下針對 [LeetCode 166. Fraction to Recurring Decimal](https://leetcode.com/problems/fraction-to-recurring-decimal/) 的實作
```c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"
struct rem_node {
int key;
int index;
struct list_head link;
};
static int find(struct list_head *heads, int size, int key)
{
struct rem_node *node;
int hash = key % size;
list_for_each_entry (node, &heads[hash], link) {
if (key == node->key)
return node->index;
}
return -1;
}
char *fractionToDecimal(int numerator, int denominator)
{
int size = 1024;
char *result = malloc(size);
char *p = result;
if (denominator == 0) {
result[0] = '\0';
return result;
}
if (numerator == 0) {
result[0] = '0';
result[1] = '\0';
return result;
}
/* using long long type make sure there has no integer overflow */
long long n = numerator;
long long d = denominator;
/* deal with negtive cases */
if (n < 0)
n = -n;
if (d < 0)
d = -d;
bool sign = (float) numerator / denominator >= 0;
if (!sign)
*p++ = '-';
long long remainder = n % d;
long long division = n / d;
sprintf(p, "%ld", division > 0 ? (long) division : (long) -division);
if (remainder == 0)
return result;
p = result + strlen(result);
*p++ = '.';
/* Using a map to record all of reminders and their position.
* if the reminder appeared before, which means the repeated loop begin,
*/
char *decimal = malloc(size);
memset(decimal, 0, size);
char *q = decimal;
size = 1333;
struct list_head *heads = malloc(size * sizeof(*heads));
for (int i = 0; i < size; i++)
INIT_LIST_HEAD(&heads[i]);
for (int i = 0; remainder; i++) {
int pos = find(heads, size, remainder);
if (pos >= 0) {
while (PPP > 0)
*p++ = *decimal++;
*p++ = '(';
while (*decimal != '\0')
*p++ = *decimal++;
*p++ = ')';
*p = '\0';
return result;
}
struct rem_node *node = malloc(sizeof(*node));
node->key = remainder;
node->index = i;
MMM(&node->link, EEE);
*q++ = (remainder * 10) / d + '0';
remainder = (remainder * 10) % d;
}
strcpy(p, decimal);
return result;
}
```
補完程式碼,並
* 解釋上述程式碼運作原理,指出其中不足,並予以改進
> 例如,判斷負號只要寫作 bool isNegative = numerator < 0 ^ denominator < 0;
> 搭配研讀 The simple math behind decimal-binary conversion algorithms
* 在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
## 測驗 `6`
`__alignof__` 是 GNU extension,以下是其可能的實作方式
```c
/*
* ALIGNOF - get the alignment of a type
* @t: the type to test
*
* This returns a safe alignment for the given type.
*/
#define ALIGNOF(t) \
((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X)
```
補完程式碼,並
* 解釋上述程式碼運作原理
* 在 Linux 核心原始程式碼中找出 `__alignof__` 的使用案例 2 則,並針對其場景進行解說
* 在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
### 填空
```graphviz
digraph g_align{
rankdir = LR
node[shape=record]
s [label="<c>char c| padding | <h>t _h "]
node[shape=plaintext]
0 -> s:c
p -> s:h
}
```
如圖,利用 struct 自動 alignment 來算出 alignment 大小
> char: 1 byte
> t: ? byte,$\geq$ 1,mod 4 = 0
> 所以會以 t 的大小作對齊
將 p 的記憶體位置減去 0 即為所求
因此 M 應填入 `_h` 以取得第三個區塊的開頭位置
而由前面 `(struct {char c; t _h} *)0` 可知 X 應填入 `0` ,才會在同個起始位置
```c
#define ALIGNOF(t) \
((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0)
```
## 測驗 `7`
考慮針對 FizzBuzz
> * 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
> * 如果是 5 的倍數,印出 “Buzz”
> * 如果是 15 的倍數,印出 “FizzBuzz”
> * 如果都不是,就印出數字本身
的實作
```c
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 << KK1) << KK2;
char fmt[9];
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length);
fmt[length] = '\0';
printf(fmt, i);
printf("\n");
}
return 0;
}
```
補完,並
* 解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
* 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
* 分析 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
* 研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
* 解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
* 過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論