# 2024q1 Homework4 (quiz3+4)
contributed by < [`SuNsHiNe-75`](https://github.com/SuNsHiNe-75) >
## 第三週測驗題
### 測驗`1`
#### 解釋程式碼運作原理
```c
int i_sqrt(int x)
{
if (x <= 1) /* Assume x is always positive */
return x;
int z = 0;
for (int m = 1UL << ((31 - __builtin_clz(x)) & ~1UL); m; m >>= AAAA) {
int b = z + m;
z >>= BBBB;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
AAAA 是 `2`;BBBB 則為 `1`。
該實作的核心思想是一直調整一「猜測值」,來**逐步逼近整數 `x` 的平方根**。
在迴圈中,先計算 `m` 的初始值(對應到公式,即首項 $d_n = (2^n)^2$)。之後將 `z` 與 `m` 相加得到 `b`,`z` 則右移一位。這表示要猜測的平方根 `z` 左移一位。如果 `x` 大於等於 `b`,則表示該猜測值「低於實際的平方根」,所以要把 `x` 減 `b`,然後將 `z` 與 `m` 相加,以調整猜測值。重複此過程,一直到 `m` 變為 `0` 為止。
#### 用 ffs 取代 `__builtin_clz`
使用到 [第 `2` 週-測驗 `3`](https://hackmd.io/@sysprog/linux2024-quiz2#%E6%B8%AC%E9%A9%97-3) 中定義 `__ffs` 函式來實現 `__builtin_clz` 的取代。
分支版本:
```c
int i_sqrt_ffs(int x)
{
if (x <= 1)
return x;
int z = 0;
for (int m = 1UL << ((31 - __ffs(x)) & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
if (x >= b)
x -= b, z += m;
}
return z;
}
```
無分支版本:
```c
int i_sqrt_ffs_branchless(int x)
{
if (x <= 1)
return x;
int z = 0;
int m = 1UL << ((31 - __ffs(x)) & ~1UL);
while (m) {
int b = z + m;
int mask = -(x >= b);
x -= b & mask;
z += m & mask;
m >>= 2;
}
return z;
}
```
為了避免使用分支,這邊使用了一個 `mask` 變數來檢查是否滿足「`x` 大於等於 `b`」,如果滿足則更新 `x` 和 `z` 的值。
---
### 測驗`2`
```c
void divmod_10(uint32_t in, uint32_t *div, uint32_t *mod)
{
uint32_t x = (in | 1) - (in >> 2); /* div = in/10 ==> div = 0.75*in/8 */
uint32_t q = (x >> 4) + x;
x = q;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
q = (q >> 8) + x;
*div = (q >> CCCC);
*mod = in - ((q & ~0x7) + (*div << DDDD));
}
```
CCCC 是 `2`;DDDD 則為 `1`。
---
### 測驗`3`
#### 解釋程式碼運作原理
考慮 $ilog2$ 可計算以 2 為底的對數,先來解釋 [第 3 週測驗題-測驗`3`](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-3) 中提到的版本二及版本三之對數函式實作,並重新填空。
版本二:
```c
static size_t ilog2(size_t i)
{
size_t result = 0;
while (i >= AAAA) {
result += 16;
i >>= 16;
}
while (i >= BBBB) {
result += 8;
i >>= 8;
}
while (i >= CCCC) {
result += 4;
i >>= 4;
}
while (i >= 2) {
result += 1;
i >>= 1;
}
return result;
}
```
AAAA 為 `65536`、BBBB 為 `256`,而 CCCC 則是 `16`。
比起版本一,該版本程式碼透過 [Bit Manipulation](https://en.wikipedia.org/wiki/Bit_manipulation) 實作 $ilog2$,以加快程式執行速度。
以「第一個 `while`」為例,首先檢查傳入的數字是否 $\geq$ 65536,如果是,就將結果 $+$ 16,然後將該數字**右移 16 位**來將數字縮小範圍,以便後續更快地計算對數。
> 從 $2^{16}$ 開始除,接著 $2^8$、$2^4$... 而不是一直除 $2$,時間複雜度太高。
版本三:
```c
int ilog32(uint32_t v)
{
return (31 - __builtin_clz(DDDD));
}
```
DDDD 為 `v | 1`。
參照 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)。
該段程式碼用來計算「32-bit 無符號整數 `v` 的二進制表示」中最高位(最左邊的位元為 1)的位置的索引。其用到了 GCC 內建函數 `__builtin_clz`,其會回傳「從參數的二進制表示開始的位元中,連續的 0-bit」的個數。
> `__builtin_clz`:Returns the number of leading 0-bits in `x`, starting at the most significant bit position. If `x` is `0`, the result is undefined.
所以,這裡的 `(v | 1)` 會確保即使 `v` 是 `0`,`__builtin_clz` 則會是 `31`(因為連續 31 個 `0`),所以最後會回傳 `0`。
總結一下,此函式就是在找「`1` 的最高位」的位置,即是某整數 $log_2$ 後並取整數的結果。
#### Linux 核心中,$log_2$ 相關程式碼分析
參照 [linux/kernel/trace/trace_events_hist.c](https://github.com/torvalds/linux/blob/39cd87c4eb2b893354f3b850f916353f2658ae6f/kernel/trace/trace_events_hist.c#L276) 中的 `hist_field_log2`,是有關 hlist 的資料數值操作函式;而其回傳值就有呼叫到 `ilog2` 函式。
```c
static u64 hist_field_log2(struct hist_field *hist_field,
struct tracing_map_elt *elt,
struct trace_buffer *buffer,
struct ring_buffer_event *rbe,
void *event)
{
struct hist_field *operand = hist_field->operands[0];
u64 val = hist_fn_call(operand, elt, buffer, rbe, event);
return (u64) ilog2(roundup_pow_of_two(val));
}
```
該 `ilog2` 函式定義在 [linux/tools/testing/selftests/mm/thuge-gen.c](https://github.com/torvalds/linux/blob/master/tools/testing/selftests/mm/thuge-gen.c#L51) 中,如下:
```c
int ilog2(unsigned long v)
{
int l = 0;
while ((1UL << l) < v)
l++;
return l;
}
```
與版本二/三的實作方法不同,此 `ilog2` 函式的關鍵在其 `while` 迴圈,`(1UL << l)` 會把 `1` 左移 `l` 位,即在計算「`2` 的 `l` 次方」。所以該迴圈就是在說-當 `(1UL << l)` 所代表的 `2` 的冪次方小於傳入數字 `v` 時,迴圈就繼續執行。換句話說,該函式在找一數字 `l`,其為「比傳入數字 `v` 大」的最小的 `2` 的冪次方。即為 `ilog2` 的邏輯。
---
### 測驗`4`
#### 解釋程式碼運作原理
首先需要先理解 Exponentially Weighted Moving Average (EWMA) 為何,可參照 [Wiki - Exponential moving average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) 及 [第 `3` 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-4)。
可以理解成一每筆資料都「附有權重」的取平均方程式,而「時間越久」則「權重比重低」。
觀察 EWMA 的可能實作程式碼,其包含了用於來計算 EWMA 的通用函數。其中有一 `ewma` 結構,包含了 EWMA 參數以及平均值的縮放內部表示,旨在**最小化四捨五入誤差**。
- `factor`:內部值表示的縮放因子。最高可達到的平均值如此公式決定:`ULONG_MAX / (factor * weight)`。為了優化性能,`factor` 必須是 2 的冪次方。
- `weight`:指數權重,也稱為衰減率,決定了舊值影響力減弱的速度。為了效率,`weight` 也必須設置為 2 的冪次方。
```c
void ewma_init(struct ewma *avg, unsigned long factor, unsigned long weight)
{
if (!is_power_of_2(weight) || !is_power_of_2(factor))
assert(0 && "weight and factor have to be a power of two!");
avg->weight = ilog2(weight);
avg->factor = ilog2(factor);
avg->internal = 0;
}
```
實作需透過 `ewma_init` 函式來**初始化縮放因子和指數加權**。
> 內部會使用到 [測驗 `3`](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-3) 的 `ilog2` 函式。
```c
struct ewma *ewma_add(struct ewma *avg, unsigned long val)
{
avg->internal = avg->internal
? (((avg->internal << EEEE) - avg->internal) +
(val << FFFF)) >> avg->weight
: (val << avg->factor);
return avg;
}
```
EEEE 為 `avg->weight`;FFFF 為 `avg->factor`。
此函式是 EWMA 實作的核心,其核心思想是**對「過去的資料」賦予指數衰減的權重,從而使得「最新的樣本值」對整個平均值的影響最大**,而過去的數據則有較小的影響,從而達到平滑序列數據的效果。
因此該段程式碼可如下解析:
- `internal` 未初始化:直接將值左移 `avg->factor` 來調整權重。
- `internal` 已作過運算:對於每筆新的資料,首先將「之前的加權平均值」左移 `avg->weight` 位,以增加其衰減效果,並從中減去這個值;然後加上「新的加權樣本值」以完成對加權平均值的更新。最後,要將計算出的新的加權平均值右移 `avg->weight` 位,來調整整體權重。
```c
unsigned long ewma_read(const struct ewma *avg)
{
return avg->internal >> avg->factor;
}
```
最後,`ewma_read` 只是將計算平均值「還原縮放」,以獲取其真正的平均值。
#### Linux 核心中,EWMA 相關程式碼分析
參照 [linux/drivers/net/wireless/ath/ath9k/dynack.c](https://github.com/torvalds/linux/blob/39cd87c4eb2b893354f3b850f916353f2658ae6f/drivers/net/wireless/ath/ath9k/dynack.c#L50),該檔案是 Atheros AR9000 無線網路設備的 **Dynamic Adaptive Noise and Channel adaptation(DYNACK)** 功能的實現。而該檔案就涉及了使用 EWMA 來根據環境條件調整傳輸功率和通道。
> 其旨在改善無線連接的穩定性和性能,提供了在不同環境條件下自動調整傳輸功率和選擇最佳通道的能力。
細讀發現,在 `ath_dynack_compute_to` 函式中,呼叫了 `ath_dynack_ewma` 等有關 EWMA 計算的函式。其是用於計算 STA (Station) 的 ACK (Acknowledgement) 超時時間 (ACK Timeout),以動態調整 STA 的 ACK 超時時間,來優化無線傳輸的效能。
重點在此段程式碼:
```c=173
sta = ieee80211_find_sta_by_ifaddr(ah->hw, dst, src);
if (sta) {
an = (struct ath_node *)sta->drv_priv;
an->ackto = ath_dynack_ewma(an->ackto, ackto);
```
主要先透過 `dst` 和 `src` 來找對應的無線站點(STA)。然後從 STA 的驅動私有數據中取得相關的 ATH 節點(`ath_node`),而後就使用到了 EWMA 相關函式 `ath_dynack_ewma` 來計算新的 ACK 超時時間。
> 這個新的超時時間是根據「舊的超時時間和最新的 ACK 接收時間之間的加權平均值」。其有助於跟蹤 ACK 超時時間的變化。
`ath_dynack_ewma` 函式實作如下:
```c
static inline int ath_dynack_ewma(int old, int new)
{
if (old > 0)
return (new * (EWMA_DIV - EWMA_LEVEL) +
old * EWMA_LEVEL) / EWMA_DIV;
else
return new;
}
```
---
### 測驗`5`
#### 解釋程式碼運作原理
此段程式碼是用來計算 $\lceil log_2(x) \rceil$ 的。
> 其中 `x` 必大於 `0`。
```c=
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
x--;
r = (x > 0xFFFF) << 4;
x >>= r;
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
shift = (x > 0xF) << 2;
x >>= shift;
r |= shift;
shift = (x > 0x3) << 1;
x >>= shift;
return (r | shift | x > GGGG) + 1;
}
```
GGGG 為 `1`。
考慮到二進制對數的定義,如果 `x` 本身是 `2` 的冪次方,那**大於或等於 `x` 的二進制對數**就是 `x` 的二進制表示中的 `1` 的個數;反之,如果 `x` 不是 `2` 的冪次方,而是介於兩個 `2` 的冪次方之間的數,那麼**大於或等於 `x` 的二進制對數**就是「`x` 的二進制表示中最高位 `1` 的位置」加 `1`。
鑒於此,此函式會先 `x--`,來確保不會計算大於 `x` 的二進制對數。
第 6 行,如果 `x` 大於 `65535`,則將 `r` 設為 `16`,否則為 `0`;並在第 7 行將 `x` 右移 `r` 位,若先前有將 `r` 設為 `16`,則此步的目的就是要將 `x` 的大小範圍縮小到 `16` 位以下。
下面 8-10、11-13 和 14-15 行同理,都是在透過位操作來計算大於或等於 `x` 的二進制對數的整數部分。