# 2024q1 Homework4 (quiz3+4)
contributed by < `vestata` >
## [第 3 周測驗題](https://hackmd.io/@sysprog/linux2024-quiz3)
### 測驗一
> [commit 401bd93](https://github.com/vestata/linux2024-homework4/commit/401bd937bcade0193b8bf36074d46a6c8fd3cc54)
這段程式碼中的 `log2(N)` 函數用來計算數字 `N` 的以 2 為底的對數。
在這個 `i_sqrt` 函數中,`log2(N)` 用於找出 `N` 的最高有效位(most significant bit, msb)。這是因為 `N` 的平方根絕不會大於 `N` 本身的二進位數值。舉例來說,如果 `N` 是 16,其二進位表示為 10000(5位),則其平方根不會超過 $2^log(N) = 16$。因此,通過 `log2(N)`,我們可以找到一個**大約**的起始點來進行平方根的計算,從而減少計算次數。
在 `i_sqrt` 函式中,`a` 被設置為 `1 << msb`,即 `2` 的 `msb` 次方。這是對 `N` 進行二進位搜索的起始點,然後逐步右移(`a >>= 1`),進行逼近計算,直到找到最接近 `N` 的平方根為止。
log 函數通常返回一個浮點數,因此這裡將結果轉換為整數(使用 (int) 強制類型轉換),依賴於標準數學庫,並涉及浮點運算。
> [版本二](https://github.com/vestata/linux2024-homework4/commit/c2327bc0a7bb895bb19d11b994b5ae3b1cc5a56b) 過程中直接在整數的二進位表示上操作,不涉及浮點運算。
#### 整理數學式
##### 自 [Digit-by-digit calculation](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation) 中:
- 先簡單舉例:
令 $Z = 10X + Y$, 平方根為 10 位數的狀況。
$Z = (10X + Y)^2 = 100X^2 + 20XY + y^2$
- 這時候使用 digit-to-digit algo
1. 先找 x 使用:每一輪 $x*2$ 使其滿足 $x^2 <= (Z >> 2)$
2. 找到最大之 X 帶入找 Y
- 延伸到所有狀況:
$N = (a_1 + a_2 + a_3 + ... a_n)^2$
又
$(a_1 + a_2 + a_3 + \dots + a_n)^2 =$
$a_1^2 + 2a_1a_2 + a_2^2 + 2(a_1 + a_2)a_3 + a_3^2 + \dots + a_{n-1}^2 + 2 \left( \sum_{i=1}^{n-1} a_i \right) a_n + a_n^2 =$
$a_1^2 + [2a_1 + a_2] a_2 + [2(a_1 + a_2) + a_3] a_3 + \dots + \left[ 2 \left( \sum_{i=1}^{n-1} a_i \right) + a_n \right]a_n$
- 再根據上面的算法由 $a_1...a_{m-1}$逐一去猜。
- 令 $a_1...a_{m-1}$ 已被猜到 $a_m$ 那一項便會是 $Y_m = [2P_{m-1} + a_m]a_m$,其中 $P_{m-1}$ 是前面已備猜到的部份。
- 所以要猜 $a_m$ 會滿足遞迴式 $X_m = X_{m-1} - Y_m$
<!--
```graphviz
digraph S{
node[shape=record]
tmp[label="a_1 |a_2|...|<a1> a_(m-1)| <a2>a_m|...|||a_n"]
tmp2[label="2 * 前面的猜測"]
}
```
-->
##### 在 Binary numeral system (base 2)之下略有不:
$N^2 = (a_n + ... + a_0)^2, a_m=2^m\,or\,a_m$
根據 [第 3 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3) 的推導:
$P_m = a_n + a_{a-1} + ... + a_m$
$P_m = a_n + a_{a-1} + ... + a_0$ 為平方根 $N$
所以便是針對 $P_m$ 逐一嘗試:
由於無法每一輪檢驗 $N^2 - P_{m}^2$ 成本過高,所以可以用跟上一輪的差值做比較。
再將 Y_m 做細分成 $c_m,d_m$
$c_m = P_{m+1}2^{m+1}$
$d_m = (2^m)^2$
在每一輪會更新 $c_m, d_m$
$c_{m-1} = P_m 2^m = \left( P_{m+1} + a_m \right) 2^m = P_{m+1} 2^{2m} + a_m 2^m =
\begin{cases}
c_m / 2 + d_m & \text{if } a_m = 2^m \\
c_m / 2 & \text{if } a_m = 0
\end{cases}$
$d_{m-1} = \frac{d_m}{4}$
最終條件會是 $c_{-1} = P_02^0 = P_0 = N$
#### 程式修正
`__builtin_clz` 之功能為回傳從前面數有多少 0
> Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
1. 針對 [第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) 之中的 `ffs` (從後面數有多少 0) 所採用 binary search 的方法做出與 `__builtin_clz` 相同功能的 `fls`
2. 針對提供分支和無分支 (branchless) 的實作,目前不確定如何實作。
> [commit 8bd78f4](https://github.com/vestata/linux2024-homework4/commit/8bd78f466272976a20c4e63513609ef2e61fde86)
在看了 linux kernel 中的 `fls` 使用發現這邊的誤用,這邊做了修正。
```diff
int z = 0;
- for (int m = 1UL << ((31 - fls(x)) & ~1UL); m; m >>= 2) {
+ for (int m = 1UL << ((fls(x) - 1) & ~1UL); m; m >>= 2) {
int b = z + m;
z >>= 1;
```
> [commit 29b0769](https://github.com/vestata/linux2024-homework4/commit/29b076981b44d573a954a4eb79a2e1e392dec5e6)
#### Linux 核心 `linux/block/blk-wbt.c`
在以下程式碼有提到:
```c
static void rwb_arm_timer(struct rq_wb *rwb)
{
struct rq_depth *rqd = &rwb->rq_depth;
if (rqd->scale_step > 0) {
/*
* We should speed this up, using some variant of a fast
* integer inverse square root calculation. Since we only do
* this for every window expiration, it's not a huge deal,
* though.
*/
rwb->cur_win_nsec = div_u64(rwb->win_nsec << 4,
int_sqrt((rqd->scale_step + 1) << 8));
} else {
/*
* For step < 0, we don't want to increase/decrease the
* window size.
*/
rwb->cur_win_nsec = rwb->win_nsec;
}
blk_stat_activate_nsecs(rwb->cb, rwb->cur_win_nsec);
}
```
`blk-wbt.c` 代表 Buffered writeback throttling 是一調節 I/O 的技術。策略如下:
- 在一個定義的時間窗口內監控延遲。
- 如果該時間窗口內的最小延遲超過某個目標值,則增加 scaling step ,並將 queue 深度縮小 2 倍。然後將時間窗口縮小到 100 / sqrt(調節步驟 + 1)。
- 對於任何沒有確鑿延遲數據的窗口,保持現狀。
- 如果延遲看起來不錯,則減小 scaling step 。
直接對應 `sqrt()` 程式:
```c
if (rqd->scale_step > 0) {
rwb->cur_win_nsec = div_u64(rwb->win_nsec << 4,
int_sqrt((rqd->scale_step + 1) << 8));
} else {
rwb->cur_win_nsec = rwb->win_nsec;
}
```
rwb->win_nsec 會初始為 100ms,但是這邊這邊跟敘述的 `100 / sqrt(調節步驟 + 1)` 實作不一樣,分別對 `rwb->win_nsec << 4`,`(rqd->scale_step + 1) << 8` ,為何要位移不知道為何作用。
:::info
TODO 去翻 block 的 commit message
:::
:::info
閱讀〈Analysis of the lookup table size for square-rooting〉和〈Timing Square Root〉,嘗試撰寫更快的整數開平分根運算程式。
:::
### 測驗二
### 測驗三
#### 解釋程式碼運作原理
- [ ] 版本一
使用最直觀的方式,使用 while loop 不斷對 i 除以 2 得出 $log_2(i)$
- [ ] 版本二
> [commit e66f2c9](https://github.com/vestata/linux2024-homework4/commit/e66f2c939765396a44738319e6b46ea8bf9dc8ef)
以 binary search 針對不同大小 i 可以一次做多位數的計算,增加其速度。
- [ ] 版本三
> [commit cb2ec77](https://github.com/vestata/linux2024-homework4/commit/cb2ec770ab8d734e23b5b30f82e046bcc6801b10)
使用 GNU extension `__builtin_clz(x)`
> Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
所以它會回傳 x 前面有多少位元為 0。如果講他視為以下格式 `__builtin_clz(x)` 便會是 n 等價於它最高位的非 0 元素是 (n-1) = 31 - n,便會是 log(n)
| 31 | 30 | ... | n | n-1 | ... | 0 |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | ... | 0 | 1 | ... | 1 |
:::warning
TODO: 分析上述實作手段的指令延遲
:notes: jserv
> 好的,以下為實作 [name=vestata]
:::
#### clock_gettime() 測量
以下分析三種實作手段的延遲差異:
> [commit 61732ea](https://github.com/vestata/linux2024-homework4/commit/61732ea61f8b0d6433030f6f297ddc11a74c67e8)
1. 修改原本的程式,加入 Designated Initializers:
```c
int main(){
Log2Impl impls[] = {
{.func = ilog2, .file_name = "out_ilog2.txt"},
{.func = ilog2_2, .file_name = "out_ilog2_2.txt"},
{.func = ilog32, .file_name = "out_ilog32.txt"}
};
for (int j = 0; j < sizeof(impls) / sizeof(impls[0]); j++) {
FILE *fp = fopen(impls[j].file_name, "w");
struct timespec start, end;
int i;
for (i = 1; i < 1000000 && i; i++) {
clock_gettime(CLOCK_MONOTONIC, &start);
impls[j].func(i);
clock_gettime(CLOCK_MONOTONIC, &end);
long long time = (long long) (end.tv_sec * 1e9 + end.tv_nsec) - \
(long long) (start.tv_sec * 1e9 + start.tv_nsec); \
fprintf(fp, "%u, %lld\n", i, time);
}
fclose(fp);
}
return 0;
}
```
2. 新增 gnuplot 腳本
3. 修改 Makefile
對 1~5000 的資料:
![log2_5000](https://hackmd.io/_uploads/B1UK8XY10.png =70%x)
對 1~10000 的資料:
![log2_10000](https://hackmd.io/_uploads/Sk_9I7Yy0.png =70%x)
對 1~1000000 的資料:
![log2_1000000_2](https://hackmd.io/_uploads/HJkiImYkA.png =70%x)
可以觀察到 ilog32 的速度跟穩定度相較 ilog2, ilog_binary 都很高,因為使用 `__builtin_clz(x)` 比起其他兩個方法還省去 bitwise 的操作,還可以快速找到 msb 直接算出 log2 答案。ilog2 在時間上的表現是最差的辦多的噪點,使用最基本的除法方式,效率會很慢。ilog_binary 因為其 binary search 的性質在較大的數值會有比較好的表現。
在這個實驗我發現使用 log2 的時間會有許多噪點,目前這些噪點出現的原因我尚未釐清。我在 gnuplot 時為了清楚的繪圖,使用 threshold 去捨棄 200ns 以上的數值。
:::info
為何有如此多噪點呢?
先思考是否為計時程式導致的問題?
:::
#### Perf 測量
所以我更新了我實驗的方式,改為使用 perf ,我使用命令 `perf stat --repeat=5 -e task-clock ./2024w3q3 method x` 有定義三種方法 0, 1, 2 分別對應上述三種方法,x 定義為 `ilog(x)` 的參數,x 大小定義為 1 - 10000,並平均五次測試的結果。
接下來撰寫了一個 bash 腳本以分別完成對三種方法的測量輸出 `csv` 檔案。
```bash
executable_name="./2024w3q3"
output_file_prefix="perf_output_function_"
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <function_index>"
exit 1
fi
function_index="$1"
output_file="${output_file_prefix}${function_index}_r5.csv"
rm -f "$output_file"
for i in $(seq 1 10000)
do
echo "Measuring performance for function index $function_index with input: $i"
perf stat --repeat=5 -e task-clock -o temp_perf_data -- "$executable_name" $function_index $i 2>/dev/null
task_clock=$(grep 'task-clock' temp_perf_data | awk '{print $1}')
echo "$i, $task_clock" >> "$output_file"
done
echo "Performance measurement for function index $function_index completed."
```
此次實驗得出的結果使用 gnuplot 繪出以下:
![image](https://hackmd.io/_uploads/rJpsEGtgC.png)
雖然使用 perf 中的 task-clock 沒有出現像上一個實驗的噪點,但是圖像中顯示 ilog32() 沒有像理論上產生如此大的優勢。我回頭思考使用 perf 作為時間測量的正確性。
perf 的 task-clock 是以一個程式為單位,但是如今我們測試目標是 ilog2() ,是一個計算速度非常快的函式,其時間只會佔執行程式非常小的部份,所以使用 perf 之 task-clock 不會是正確的選擇。
#### 實驗結論
所以實驗結果還是依據還是使用 `clock_gettime()` 但是噪點原因還在分析中。但就算有噪點,但是因為 ilog2() 運算速度極快,對整個程式影響不大。
附上三種方法 perf 10000 筆資料平均的比較:
```
The average time for ilog2() is 0.268591 ms
The average time for ilog2_2() is 0.264863 ms
The average time for ilog32() is 0.262433 ms
```
#### Linux 核心原始程式碼
在 [include/linux/log2.h](https://github.com/torvalds/linux/blob/712e14250dd2907346617eba275c46f53db8fae7/include/linux/log2.h#L31) 中可以見到它取 `ilog2` 使用的方式是調用 fls() 來進行,它可以快速取的最大的非 0 位置, `fls(x) - 1` 便是 $log_2(x)$
我們可以看到它分定義針對 32 位元架構 `__ilog2_u32` 與 64 位元架構的 `__ilog2_u64`,皆是採用 fls 的作法。
之後會在主要 `ilog2(n)` 巨集被調用
```c
#define ilog2(n) \
( \
__builtin_constant_p(n) ? \
((n) < 2 ? 0 : \
63 - __builtin_clzll(n)) : \
(sizeof(n) <= 4) ? \
__ilog2_u32(n) : \
__ilog2_u64(n) \
)
```
`__builtin_constant_p` 是 GCC 編譯器提供的一個內建函數,用來檢查其參數是否是在編譯時已知的常數表達式。如果參數是一個常數表達式,則 `__builtin_constant_p` 返回 1;如果參數不是常數表達式,則返回 0。而常數表達式表示該便書在編譯期間不會改變,提高程式碼的效率和執行速度。
如果是常數便進入 `(n) < 2 ? 0 : 63 - __builtin_clzll(n)) :` 如果 n 是 0 或是 1 便直接回傳 0,再來檢查 n 的大小如果他是 32 位元 ((sizeof(n) <= 4)) 便使用 __ilog2_u32(n),若不是則使用 __ilog2_u64(n) 。
### 測驗四
#### EWMA
原理詳見 [第 3 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-4)
觀察 EWMA 的可能實作中提到:
- 主要針對降低 rounding 所帶來的誤差。
- 透過 Scaling factor 使 internal 可以更準確的運算。
- 有 `ewma_read()` 來輸出 internel 的值,不要直接存取結構體中的 internel。
##### struct ewma
```c
/* Exponentially weighted moving average (EWMA) */
struct ewma {
unsigned long internal;
unsigned long factor;
unsigned long weight;
};
```
- internal:$S_t$
- factor:scaling factor 用途
- EWMA 結構可以表示的最大平均數值。這個值由公式 `ULONG_MAX / (factor * weight)` 決定,其中 ULONG_MAX 是 unsigned long 數據類型能夠表示的最大值。這個公式顯示了 scaling factor 和權重如何共同影響可表示的最大平均值。
- weight:$\alpha$ 用於衰減舊數據影響的指數權重
##### `ewma_init`
```c
if (!is_power_of_2(weight) || !is_power_of_2(factor))
assert(0 && "weight and factor have to be a power of two!");
```
限制為 2 的冪主要是基於性能和計算效率的考慮。
##### `ewma_add()`
* @avg: Average structure
* @val: Current value
```c
struct ewma *ewma_add(struct ewma *avg, unsigned long val)
{
avg->internal = avg->internal
? (((avg->internal << avg->weight) - avg->internal) +
(val << avg->factor)) >> avg->weight
: (val << avg->factor);
return avg;
}
```
這邊 `ewma_add()` 按照上述取平均的統計手法,根據 $S_t = \alpha Y_t + (1 - \alpha)*S_{t-1}$ 進行計算,對應到上述程式如以下:
| $(1 - \alpha)*S_{t-1}$ | $\alpha Y_t$ |
| -------------------------------------------------- | -------------------------------------- |
| `((avg->internal << avg->weight) - avg->internal)` | `(val << avg->factor)) >> avg->weight` |
注意:這邊換進去 internal 都進行 factor scaling
#### Linux 核心無線網路裝置驅動程式
針對 `linux/drivers/net/wireless` 去做探討。
EWMA 常被用於無線網絡驅動開發中,特別是用於信號強度的測量、網絡品質的評估,以及調整和優化網絡性能。
:::danger
巨集無法「呼叫」(call),因其本質的操作是「展開」(expand),注意用語。
:notes: jserv
> 收到!會注意的 [name=vestata]
:::
可以大量<s>調用</s> DECLARE_EWMA 巨集,找到位在 `include/linux/average.h` 中對 ewma 的定義。
- 其中有三個參數:
1. `name`:
2. `_precision`:這邊相當於上邊所訴 scaling factor
3. `_weight_rcp`:這邊相當於上面所訴的權重,$\alpha$
- 在這邊對於 name 使用 `##` token pasting。
- 例如,如果使用 `DECLARE_EWMA(avg_rssi, 10, 8)`,##name 會將 name 替換為 `avg_rssi`,從而生成 `ewma_avg_rssi`、`ewma_avg_rssi_init`、`ewma_avg_rssi_read` 和 `ewma_avg_rssi_add`,詳見 [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor?type=view#%E9%96%8B%E7%99%BC%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%A8%8B%E5%BC%8F%E6%99%82%EF%BC%8C%E5%96%84%E7%94%A8%E5%89%8D%E7%BD%AE%E8%99%95%E7%90%86%E5%99%A8%E5%8F%AF%E5%A4%A7%E5%B9%85%E7%B0%A1%E5%8C%96%E9%96%8B%E7%99%BC)
- 可以在 `include/linux/average.h` 中看到 `ewma_##name##_add`, `ewma_##name##_read`中類似於剛剛提到的例題相同的操作。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
> 收到,未來會注意的! [name=vestata]
:::
而在 `linux/drivers/net/wireless` 這個目錄中觀察到針對:`avg_rssi`, `beacon_rssi`, `rate`, `signal`等等進行 DECLARE_EWMA
- 針對 `ath11k/` 進行實際應用介紹
- 在 mac.c 中 `ath11k_mac_op_sta_statistics` 函數中,`ewma_avg_rssi_read` 被用來獲取當前的 EWMA 值。這個值反映了考慮到之前的 RSSI 測量後的平均信號強度,用於更新 sinfo 中的 signal_avg 。據接收到的數據包信息更新無線站點的統計和信號質量指標,其中包括實時更新 EWMA 計算的 RSSI 值,從而提供對無線信號質量的動態評估。
- 在 `dp_rx.c` 使用 `ewma_avg_rssi_add(&arsta->avg_rssi, ppdu_info->rssi_comb)` 更新 EWMA 平均值。這一步將當前的 RSSI 值加入到 EWMA 計算中 (arsta 為一個 ath10k_sta 結構體的變數,裡面存放各種無線連接訊息)
:::info
TODO
1. 嘗試更深入理解網路 `drivers/` 實際操作
:::
### 測驗五
#### 解釋程式碼運作原理
計算 $\lceil log_2(x) \rceil$,第一眼看這個程式碼真的是毫無頭緒。
```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 > 1) + 1;
}
```
先逐一分解程式:
1. `x--`:為了針對處理 x 本身是 2 的冪的清況,若不做此處理,最後一步 ceiling + 1 會導致推到下一個對數。
2. 由於是 `unsign integer 32 bit`,先查看 x 的最高為元是在前 16 位元還是後 16 位元。其中 `<< 4` 是代表如過 x 在高位元,我們要對 x 右移 16 位元。
- **可理解為將位移量存在 r 之中。**
```c
r = (x > 0xFFFF) << 4;
x >>= r;
```
3. 在這邊 shift 作為變量使 x 移動 shift 位元,在此處 r 改為**儲存位移總數**的變量。
```c
shift = (x > 0xFF) << 3;
x >>= shift;
r |= shift;
```
4. 同 2. 範圍在 0xF
5. 最後一步 `(r | shift | x > 1) + 1` 是多步驟寫在一起可以以下拆解:
- `r | shift`:程式進行到這等價於 `r |= shift`。
- `(r | shift | x > 1)`:最後做 `if (x > 1)` 的判斷,如果為真會加到 r 的位移總量。
- `+1`:取 ceiling。
#### 改進程式碼
使可以處理 `x = 0` ,並仍是 branchless 這個問題 $\equiv$ 不要使用 `x--;` 處理 2 的冪問題 $\equiv$
在這邊發現另一個問題:
| x | 0 | 1 | 2 | 3 | 4 | ... |
| ---------- | ---------------------------- | --------------------------- | --- | --- | --- | --- |
| $log_2(x)$ | <font color="#f00">32</font> | <font color="#f00">1</font> | 1 | 2 | 2 | ... |
所以有 `log0 = 32`, `log1 = 1` 共兩個問題。
1. 針對 `log0` 做修正
2.
> [commit 7d30243](https://github.com/vestata/linux2024-homework4/commit/7d30243e355fe648ff4f467e3b480b451e982218)
使用 bitwise 操作:
```diff
uint32_t r, shift;
- x--;
+ x -= !!x;
r = (x > 0xFFFF) << 4;
```
使用此方法 `log0 = 0`, `log1 = log2 = 1` 尚有錯誤。
#### Linux 核心原始程式碼
根據提示到 Linux 核心排程器 `kernel/sched/` 尋找:
## [第 4 周測驗題](https://hackmd.io/@sysprog/linux2024-quiz4)
### 測驗一
popcount 是計算數值的二進位表示中,有多少位元是 1。
- 版本一
使用的是 naive 手段,`v &= (v - 1)` 會在每一輪將位置最低的 1 清為 0。又每一次操作會有一個 n 作為計數,這邊使用 `n = -(~n)` $\equiv$ `n++` 實踐,是一個活用二補數系統的方法。詳細可以見 [第 4 周測驗題](https://hackmd.io/@sysprog/linux2024-quiz4) 。
- 版本二
改寫為 branchless 的手法
使用數學式:
$popcount(x) = x - \lfloor \frac{x}{2} \rfloor - \lfloor \frac{x}{4} \rfloor - ... - \lfloor \frac{x}{2^{31}} \rfloor$
以下皆為二進位去觀察:
令 $x = b_{31}...b_2b_1b_0$
:::info
TODO 完成 `__builtin_popcount` 的原理解析。
:::
hammind distance: 為二進位的每個位元的差,而 `a ^ b`,XOR 操作留下相異的位元,取其 popcount 便是 hamming distance.
考慮 LeetCode 477. Total Hamming Distance 這提的目標是求所有點之間的 hamming distance 之總和。
測驗題提供的程式碼:
```c
int totalHammingDistance(int* nums, int numsSize)
{
int total = 0;;
for (int i = 0;i < numsSize;i++)
for (int j = 0; j < numsSize;j++)
total += __builtin_popcount(nums[i] ^ nums[j]);
return total >> 1;
}
```
這是使用暴力法使用二層 for 迴圈計算所有 pair,所以每個 pair 會計算**兩次** hamming distance。所以最後一步再回傳質需要再除以二已得到正確答案。
此方法在 LeetCode 477 會 `Time Limit Exceeded`。
修正此暴力法:
```diff
- for (int i = 0;i < numsSize;i++)
+ for (int i = 0;i < numsSize - 1;i++)
- for (int j = 0; j < numsSize;j++)
+ for (int j = i+1; j < numsSize;j++)
total += __builtin_popcount(nums[i] ^ nums[j]);
- return total >> 1;
+ return total;
```
可以通過 LeetCode 477 但是時間表現在最後 20%,尚有進步空間。
找到其規律:
| | 4 | 3 | 2 | 1 | 0 |
| ------ | --- | --- | --- | --- | --- |
| 7 | 0 | 0 | 1 | 1 | 1 |
| 5 | 0 | 0 | 1 | 0 | 1 |
| 10 | 0 | 1 | 0 | 0 | 1 |
| 17 | 1 | 0 | 0 | 0 | 1 |
| result | 3 | 3 | 4 | 3 | 0 |
我們使用列的視角去觀看二進位表示。根據上面暴力法我們使用的 XOR 操作是針對每一位數有多少相異的位元。以上面的例子(依據上面列的標示):
第 0 列有 4 個 1,其 XOR 結果也都會是 0,所以最後再計算 popcount 也會是 0。
第 1 列有一個 1 與 三個 0 ,在整個遍歷會出現 (1 ^ 0) 三次,所以總共的 popcount 次數會有 3 次。
第 2 列 有兩個 1 兩個 0,會分別有 ((1 ^ 0), (1 ^ 0)), ((1 ^ 0), (1 ^ 0))總共四次,所以在 popcount 也會增加 4 次。
以此類推...
同理我們可以每一列將它拆成:x 個 1 與 y 個 0,將 x * y 便是上面的結果(這是觀察的結果),也可以說是上面做法的歸納。
實作如以下:
```c
int totalHammingDistance(int* nums, int numsSize) {
int i, j, k, t;
t = 0;
for (i = 0; i < 32; i++) {
k = 0;
for (j = 0; j < numsSize; j++) {
k += (nums[j] & (1U << i)) ? 1 : 0;
}
t += k * (numsSize - k);
}
return t;
}
```
k 為 1 的計數,而整列不是 1 就是 0 所以使用 (numsSize - k) 作為0的數量。
在 leetcode 上測試使用 (1 << i) 會出現以下錯誤:
```shell
left shift of 1 by 31 places cannot be represented in type 'int'
```
原本 1 預設為 int 不能進行 << 31 操作,要使用 `1U` unsigned int 才可以。
但是而在本地電腦 `gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0` (1 << 31)可以正常編譯。也查看了 AddressSanitizer 也尚未發現問題。
:::info
TODO 為何會這樣?
:::
### 測驗二
目標:不使用任何除法就算出某數除以另一個數的餘數
\begin{align*}
2^k
\equiv
\begin{cases}
1 \pmod{3}, & \text{if } k \text{ is even} \\
-1 \pmod{3}, & \text{if } k \text{ is odd}
\end{cases}
\end{align*}
:::info
TODO 理解這段程式,真看不懂
:::
### 測驗三
考慮 XTree,XTree 的平衡機制與 AVL tree 相似,利用 hint 來評估是否需要做 rotate,然而 xt_node 的 hint 的計算方式是,其左右節點 hint 最大值 +1 並且只有在被呼叫到 xt_update 時才會被更新。
hint 怎麼去評估的?
近似而非精確:在 XTree 中,每個節點的提示值用於判斷樹的平衡狀態,但這個值並不需要與實際的最長子節點鏈長度完全一致。這樣可以減少在每次樹的修改操作後更新高度信息所需的計算量。
平衡操作的依據:當執行插入或刪除操作時,XTree 通過比較節點的提示值來決定是否需要進行旋轉來重新平衡樹。如果提示值顯示一側的鏈比另一側長得多,則可能需要進行平衡操作。
#### 程式碼解釋
##### treeint.c
`treeint_ops` 五種操作,再接上 `xt_ops` 對 `treein_xt` 進行操作。
`bench` 計算執行時間。
選擇 `xt_ops` 樹操作
通過 `ops->init` 初始化樹結構,並將其地址存儲在 `ctx` 中。
進行插入操作,根據種子是否為零使用線性或隨機方式生成元素值,測量並輸出每次插入操作的時間。
進行查找操作,方法同插入操作,測量並輸出每次查找操作的時間。
進行刪除操作,方法同插入和查找操作,測量並輸出每次刪除操作的時間。
最後,銷毀樹結構並結束程序。
> Additionally, if the node's hint becomes zero or experiences a change compared to its previous state during the update, modifications are made to the node's parent, as it existed before these update operations.
#### xtree.c
XTree 的操作基於四個基本的二元搜尋樹操作:rotate_left、rotate_right、replace_right、replace_left。
`xt_dir` :指示方向(左、右、無)。
插入:按照二元搜尋樹的標準方法插入新節點,然後調用 `xt_update` 來保持或恢復平衡。
`xt_balance` 如果左子節點存在,則計算左子樹的高度(l),加上左子節點的 hint 屬性值加 1(hint 是用於存儲或表示節點高度或深度的字段)。
#### treeint_xt.c
如何將 XTree 整合為一個特定於**整數**的二元搜尋樹