---
tags: linux2022
---
# 2022q1 Homework2 (quiz2)
contributed by < `SmallHanley` >
> [linux2022-quiz2](https://hackmd.io/@sysprog/linux2022-quiz2)
## 測驗 `1`
:::success
解釋上述程式碼運作的原理
:::
考慮以下對二個無號整數取平均值的程式碼:
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a + b) / 2;
}
```
這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow:
```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) + (a & b & 1);
}
```
**原理:**
如果是我們平常使用的兩實數取平均,可以寫成:
\begin{gather}
\frac{a + b}{2} = \frac{a}{2} + \frac{b}{2}
\end{gather}
而上述的程式碼考慮的是非負整數,因此我們可以使用右移運算子 `>>` 代替上式的除以 2。不過這還不是等價的程式碼,會忽略到兩數都是奇數時的進位問題,因此要加上 `a & b & 1`,使得當兩數都是奇數時會再加一。
我們再次改寫為以下等價的實作:
```c
#include <stdint.h>
uint32_t average(uint32_t a, uint32_t b)
{
return (a & b) + ((a ^ b) >> 1);
}
```
**原理:**
上述程式碼讓我們很容易聯想到加法器,我們先觀察一下半加器,以下是真值表 ({C, S} = A + B):
| A | B | C | S |
| --- | --- | --- |:---:|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 0 |
得到以下結果:
\begin{gather}
S = A \oplus B \\
C = A \times B
\end{gather}
而全加器則是除了兩個一位元的輸入訊號,還需要低一位的進位訊號 (即 {C~out~, S} = A + B + C~in~)。當我們要計算多位元的非負整數加法,可以利用多個全加器串接 (或稱級聯,cascade)。若要使用到半加器的結果,當前位數的運算結果,可以看作是當前位數的兩輸入訊號相加再加上前一位的進位訊號 (即 S + C~in~,注意,這裡的 S 為半加器的 Sum,C~in~ 為前一位的 C~out~),也就是當前位數兩輸入訊號的 bitwise XOR 加上 前一位數兩輸入訊號的 bitwise AND。因此,兩多位數 (分別為 a 和 b) 相加的結果可以寫作:
```c
((a & b) << 1) + (a ^ b)
```
兩數平均可以寫作:
```c
(a & b) + ((a ^ b) >> 1)
```
:::success
比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 [CS:APP 第 3 章](https://hackmd.io/@sysprog/CSAPP-ch3))
:::
==暫略==,我目前沒有觀察到什麼有趣的現象。
:::success
研讀 Linux 核心原始程式碼 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h),探討其 [Exponentially weighted moving average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) (EWMA) 實作,以及在 Linux 核心的應用
> 移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的[閾值](https://terms.naer.edu.tw/detail/1085244/)取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
:::
在 [include/linux/average.h](https://github.com/torvalds/linux/blob/master/include/linux/average.h) 中使用巨集 `DECLARE_EWMA(name, _precision, _weight_rcp)` 用來生成相關的物件和函式。其中,`_precision` 是用來表示小數部份的精準度,也就是我們使用多少位元表示。而 `_weight_rcp` 則是權重。
而巨集內部包含以下結構體:
```c
struct ewma_##name { \
unsigned long internal; \
};
```
以及以下函式:
* **void ewma_##name##_init(struct ewma_##name *e)***
將 `internal` 初始化為 `0`。
* **unsigned long ewma_##name##_read(struct ewma_##name *e)***
將 `internal` 右移 `_precision` 的位元數回傳。
* **void ewma_##name##_add(struct ewma_##name *e, unsigned long val)***
將 `val` 新增進來,新的 `internal` 會是舊的 `internal` 和 `val` 的線性組合 (`val` 佔 1 / _weight_rcp,舊的 `internal` 佔 1 - 1 / _weight_rcp)。
舊的 `internal` 佔比的部份,原本應寫作:
\begin{gather}
\mbox{(internal << precision) - (internal << precision >> weight_rcp)}
\end{gather}經化簡可寫成:
\begin{gather}
\mbox{(internal << weight_rcp) - internal}
\end{gather}
**應用:**
比方說 [drivers/net/wireless/realtek/rtw88/main.h](https://github.com/torvalds/linux/blob/8efd0d9c316af470377894a6a0f9ff63ce18c177/drivers/net/wireless/realtek/rtw88/main.h) 有使用到:
```c
DECLARE_EWMA(tp, 10, 2);
struct ewma_tp tx_ewma_tp;
```
可以推測跟一些訊號處理有關係。
## 測驗 `2`
:::success
解釋上述程式碼運作的原理
:::
改寫〈[解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation)〉一文的「不需要分支的設計」一節提供的程式碼 `min`,我們得到以下實作 (`max`):
```c
#include <stdint.h>
uint32_t max(uint32_t a, uint32_t b)
{
return a ^ ((a ^ b) & -(a < b));
}
```
**原理:**
我們知道 a 和 b 做 bitwise XOR,再和 a 做 bitwise XOR 會得到 b,因此我們可以運用這個原理讓 a 和 (a ^ b) 或是 0 做 bitwise XOR 而分別得到 b 和 a。至於要怎麼做到不使用分支,我們可以利用 a < b 的結果,加上負號,會產生兩種結果 (0x00...00 和 0xff...ff),再加上 bitwise AND 可以巧妙的做到無分支判斷最大值。
:::success
針對 32 位元無號/有號整數,撰寫同樣 [branchless](https://en.wikipedia.org/wiki/Branch_(computer_science)) 的實作
:::
當我們要判斷一個 32 位元無號整數是否為 2 的冪,我們可以用以下程式碼判斷:
```c
#include <stdint.h>
#include <stdbool.h>
bool is_power_of_2(uint32_t n)
{
uint32_t t = 0x80000000;
while (t) {
if (n == t) {
return true;
}
t >>= 1;
}
return false;
}
```
我們可以改寫成以下 branchless 的實作:
```c
#include <stdint.h>
#include <stdbool.h>
bool is_power_of_2(uint32_t n)
{
return !(n & (n - 1)) && n;
}
```
:::success
Linux 核心也有若干 branchless / branch-free 的實作,例如 [lib/sort.c](https://github.com/torvalds/linux/blob/master/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` 檢索。
:::
在 [Replace int2float() with an optimized version.](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=747f49ba67b8895a5831ab539de551b916f3738c) (commit 747f49ba) 中,將 32 位元整數的 bit pattern 轉成浮點數的 bit pattern,有使用到類似的實作手法 (不過目前 r600_blit 相關程式已背棄用)。以下是 [IEEE 754-2008](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=4610935) 定義的 `binary32` 格式,其中藍、綠、紅分別代表 sign、exponent 和 fraction,分別佔用 1、8、23 位元:
```graphviz
digraph G
{
n
[
shape = none
label = <<table border="0" cellspacing="0"><tr>
<td border="1" bgcolor="blue">0</td>
<td border="1" bgcolor="green">1</td>
<td border="1" bgcolor="green">1</td>
<td border="1" bgcolor="green">0</td>
<td border="1" bgcolor="green">1</td>
<td border="1" bgcolor="green">1</td>
<td border="1" bgcolor="green">0</td>
<td border="1" bgcolor="green">0</td>
<td border="1" bgcolor="green">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">1</td>
<td border="1" bgcolor="red">0</td>
<td border="1" bgcolor="red">0</td>
</tr></table>>
]
}
```
完整實作如下:
```c
/* 23 bits of float fractional data */
#define I2F_FRAC_BITS 23
#define I2F_MASK ((1 << I2F_FRAC_BITS) - 1)
/*
* Converts unsigned integer into 32-bit IEEE floating point representation.
* Will be exact from 0 to 2^24. Above that, we round towards zero
* as the fractional bits will not fit in a float. (It would be better to
* round towards even as the fpu does, but that is slower.)
*/
uint32_t int2float(uint32_t x)
{
uint32_t msb, exponent, fraction;
/* Zero is special */
if (!x) return 0;
/* Get location of the most significant bit */
msb = __fls(x);
/*
* Use a rotate instead of a shift because that works both leftwards
* and rightwards due to the mod(32) behaviour. This means we don't
* need to check to see if we are above 2^24 or not.
*/
fraction = ror32(x, (msb - I2F_FRAC_BITS) & 0x1f) & I2F_MASK;
exponent = (127 + msb) << I2F_FRAC_BITS;
return fraction + exponent;
}
```
其中用到一個 `ror32` 的函式,是被定義在 [include/linux/bitops.h](https://github.com/torvalds/linux/blob/a48b0872e69428d3d02994dcfad3519f01def7fa/include/linux/bitops.h#L116),程式碼如下:
```c
static inline __u32 ror32(__u32 word, unsigned int shift)
{
return (word >> (shift & 31)) | (word << ((-shift) & 31));
}
```
有些指令集架構有支援 rotate 指令,比方說 [x86_64 指令集](https://en.wikipedia.org/wiki/X86_instruction_listings) 具備 `ror`、`rol` 指令,上述程式碼經過編譯器最佳化,確實會產生出 `ror` 指令。
新版 `int2float` 的程式碼先使用 `__fls` 來找出該整數的指數部份。原本的實作 fraction 會用左移加上條件判斷來求值,新版使用 `(msb - I2F_FRAC_BITS) & 0x1f)`,搭配 `ror32` 來實作,減少檢查是否大於 $2^{24}$ 所需的分支,前者利用 unsigned integer 減法的溢位來決定輸入小於 $2^{24}$ 時的位移數目,搭配後者 rotate 指令,不管輸入大於或是小於 $2^{24}$,皆可以做到保留 MSB 之後的 23 個位元給 `fraction` 的效果。
## 測驗 `3`
:::success
解釋上述程式運作原理;
:::
考慮以下實作:
```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 (v);
return u << shift;
}
```
性質:
1. If both x and y are 0, gcd is zero $gcd(0, 0) = 0$.
2. $gcd(x, 0) = x$ and $gcd(0, y) = y$ because everything divides $0$.
3. If x and y are both even, $gcd(x, y) = 2 * gcd(\frac{x}{2}, \frac{y}{2})$ because $2$ is a common divisor.
4. If x is even and y is odd, $gcd(x, y) = gcd(\frac{x}{2}, y)$.
5. On similar lines, if x is odd and y is even, then $gcd(x, y) = gcd(x, \frac{y}{2})$. It is because $2$ is not a common divisor.
一開始的 if 判斷式對應到性質 1、2,如果 x、y 皆為 0,回傳 0;如果 y 為 0,回傳 x;如果 x 為 0,回傳 y。接著,for 迴圈對應到性質 3,並用 `shift` 記錄乘了多少次 2。while 迴圈對應到性質 4。do while 迴圈則是用到性質 5 以及 [Euclid's_algorithm](https://en.wikipedia.org/wiki/Greatest_common_divisor#Euclid's_algorithm)。做到最後 `u` 和 `v` 會相等,會跳到 else 區塊,做完該區塊後 `u` 為一非 0 數值,`v` 為 0,跳出迴圈,再將 `u` 左移 `shift`。
## 測驗 `4`
:::success
解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
:::
在影像處理中,[bit array](https://en.wikipedia.org/wiki/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;
}
```
考慮 GNU extension 的 [`__builtin_ctzll`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 的行為是回傳由低位往高位遇上連續多少個 `0` 才碰到 `1`。
> 範例: 當 `a = 16`
`16` 這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000
從低位元 (即右側) 往高位元,我們可發現 $0 \rightarrow 0 \rightarrow 0 \rightarrow 0 \rightarrow 1$,於是 ctz 就為 4,表示最低位元往高位元有 4 個 `0`
用以改寫的程式碼如下:
```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;
}
```
在 `naive()` 這個函式中,需要將 bitmap 中的所有 `1` 的位置記錄到 `out` 中,並回傳 `pos` 代表 `out` 的 size。其中,我們可以使用 `__builtin_ctzll` 優化,如 `improved()` 函式所示。使用 `ctz` 找到目前 bitmap 中的 least significant `1`,並將該 `1` clear,進行下一次迭代。我們可以觀察到 `bitset ^= t` 這一行程式就是 clear bit 的操作。至於 `t = bitset & -bitset` 則是利用二補數的特性 (可觀察 [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation) 的圖),取二補數相當於對原數作 bitwise NOT 後加 1。二補數再和原數作 bitwise AND 可以得到一個數,該數的第 r 個 bit 為 set,其餘為 clear (等價於 `1 << r`)。
:::success
設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 [bitmap density](http://www.vki.com/2013/Support/docs/vgltools-3.html);
:::
:::success
思考進一步的改進空間;
:::
可改寫成以下等價實作:
```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) {
int r = __builtin_ctzll(bitset);
out[pos++] = k * 64 + r;
bitset ^= 1UL << r;
}
}
return pos;
}
```
:::success
閱讀 [Data Structures in the Linux Kernel](https://0xax.gitbooks.io/linux-insides/content/DataStructures/linux-datastructures-3.html) 並舉出 Linux 核心使用 bitmap 的案例;
:::
Linux 的 [kernel/sched/sched.h](https://elixir.bootlin.com/linux/latest/source/kernel/sched/sched.h#L254),有關 RT scheduling 就有使用到 bitmap:
```c
/*
* This is the priority-queue data structure of the RT scheduling class:
*/
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};
```
搭配下述程式碼可以找出 bitmap 中的 first set:
```c
/*
* Every architecture must define this function. It's the fastest
* way of searching a 100-bit bitmap. It's guaranteed that at least
* one of the 100 bits is cleared.
*/
static inline int sched_find_first_bit(const unsigned long *b)
{
#if BITS_PER_LONG == 64
if (b[0])
return __ffs(b[0]);
return __ffs(b[1]) + 64;
#elif BITS_PER_LONG == 32
if (b[0])
return __ffs(b[0]);
if (b[1])
return __ffs(b[1]) + 32;
if (b[2])
return __ffs(b[2]) + 64;
return __ffs(b[3]) + 96;
#else
#error BITS_PER_LONG not defined
#endif
}
```