owned this note
owned this note
Published
Linked with GitHub
# 2024q1 Homework4 (quiz3+4)
contributed by < [Ackerman666](https://github.com/Ackerman666) >
## 第三週測驗題
### [測驗 `2`](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-2)
#### 解釋運作原理
參考 [Hacker's Delight](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf),採用 bitwise operation 來實作,可比用 division operator 來得有效率
考慮到 $\frac{n}{10}$,可以找到一組算式來逼近結果 : $n \times \frac{a}{2^{N}}$,而題目在此挑選 a = 13, N = 7
```c
void divmod_10(uint32_t n, uint32_t *div, uint32_t *mod)
{
unsigned d0 = n & 0b1;
unsigned d1 = n & 0b11;
unsigned d2 = n & 0b111;
*div = ((((n >> 3) + (n >> 1) + n) << 3) + d0 + d1 + d2) >> 7;
*mod = n - (((*div << 2) + *div) << 1);
}
```
$n \times \frac{13}{2^{7}}$ 可改寫成 $n \times 13 \div 8 \div 16$,而 $n \times 13 \div 8$ 可用下面式子求得
((n >> 3) + (n >> 1) + n) << 3
但除法位移過程中,會遺失後面三個位元的數值,所以用三個變數來保留該資訊
d0 = n & 0b1;
d1 = n & 0b11;
d2 = n & 0b111;
將遺失的數值加回去,再除以16
q = ((((n >> 3) + (n >> 1) + n) << 3) + d0 + d1 + d2) >> 7;
最後再由餘數定理來求得餘數
r = n - (((q << 2) + q) << 1);
#### 不依賴任何除法指令的 modulo 5
觀察以下規律,可以看出底數為2時,指數每 4 個數在 mod 5 的情況下會完成一次循環
> $2^0$ mod 5 = 1
> $2^1$ mod 5 = 2
> $2^2$ mod 5 = 4
> $2^3$ mod 5 = 3
> $2^4$ mod 5 = 1
> $2^5$ mod 5 = 2
> ...
因此可以建 `table[32]` 紀錄$2^i$ mod 5 的數值
```c
int table[32] = {1,2,4,3,
1,2,4,3,
1,2,4,3,
1,2,4,3,
1,2,4,3,
1,2,4,3,
1,2,4,3,
1,2,4,3,};
```
對於每個整數 n,皆可以用 2 的冪次相加來代表
因此可以逐一檢查 n 的每個位元,如果為 1 就查表並將數值加進 `mod`
`!!((5 - mod - 1) & 0x80000000)` 用來判斷當前 `mod` 有沒有 >= 5,如有,則將 5 減去
```c
int mod = 0, index = 0;
while(n){
mod += (n & 0b1) * table[index];
mod -= !!((5 - mod - 1) & 0x80000000) * 5;
n = n >> 1;
++index;
}
```
#### 不依賴任何除法指令的 modulo 9
同 modulo 5 觀察規律
> $2^0$ mod 9 = 1
> $2^1$ mod 9 = 2
> $2^2$ mod 9 = 4
> $2^3$ mod 9 = 8
> $2^4$ mod 9 = 7
> $2^5$ mod 9 = 5
> $2^6$ mod 9 = 1
> ...
建 `table[36]` 紀錄$2^i$ mod 9 的數值
```c
int table[36] = {1,2,4,8,7,5,
1,2,4,8,7,5,
1,2,4,8,7,5,
1,2,4,8,7,5,
1,2,4,8,7,5,
1,2,4,8,7,5};
```
後續求餘過程和 modulo 5 類似
> 完整程式碼 : [2b59e70](https://github.com/Ackerman666/quiz3-4/commit/2b59e7062c9c32628e2243df9a2716499af3be35)
#### 實驗
我將自定義的 `mod5` 和 `mod9` 分別執行 1000000 次,並用 perf 進行分析
166.60 msec task-clock # 0.998 CPUs utilized
2 context-switches # 12.005 /sec
0 cpu-migrations # 0.000 /sec
50 page-faults # 300.118 /sec
7,6166,0412 cycles # 4.572 GHz
8,8298,9060 instructions # 1.16 insn per cycle
9573,2445 branches # 574.620 M/sec
813,6217 branch-misses # 8.50% of all branches
`i % 5` 和 `i % 9` 分別執行 1000000 次
2.15 msec task-clock # 0.842 CPUs utilized
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
50 page-faults # 23.296 K/sec
662,0116 cycles # 3.084 GHz
398,7849 instructions # 0.60 insn per cycle
117,7528 branches # 548.642 M/sec
5953 branch-misses # 0.51% of all branches
可看出自定義函式執行速度最慢,原因是函式有用到 while 迴圈,導致 branches 數量大增 (待改進)
### [測驗 `3`](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-3)
#### 解釋運作原理
透過將 `i` 不斷往右 shift,可以得到 `i` 最高 set bit 為 1 的索引
該索引即是$\log_2 i$ 取整結果
```c
int ilog2(int i)
{
int log = -1;
while (i) {
i >>= 1;
log++;
}
return log;
}
```
透過 binary search 的方式,來加速尋找最高 set bit 的過程
> 設 i = fx00FFFFFF
> i >= 65536,代表最高 set bit 的索引 >= 16
> 將 result + 16,並對 i 往右位移 16 位元,並往下進行後續判斷,來找到最高 set bit 所在索引
```c
static size_t ilog2(size_t i)
{
size_t result = 0;
while (i >=65536) {
result += 16;
i >>= 16;
}
while (i >= 256) {
result += 8;
i >>= 8;
}
while (i >= 16) {
result += 4;
i >>= 4;
}
while (i >= 2) {
result += 1;
i >>= 1;
}
return result;
}
```
透過 gcc 內建 `__builtin_clz(unsigned int 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.
>
>計算 leading 0-bits 個數,x = 0 時未定義
```c
int ilog32(uint32_t v)
{
return (31 - __builtin_clz(v | 1));
}
```
`__builtin_clz(v | 1)` 讓傳遞進的參數值至少為1,來避免未定義情況。而 v | 1 並不會影響正確性
因 `__builtin_clz(v)` 和 `__builtin_clz(v | 1)` 在 v 不等於 0 時結果都一樣
> v = 0010$_2$ leading 0-bits 數量為 2
> v|1 = 0011$_2$ leading 0-bits 數量也為 2
透過 `31 - __builtin_clz(v | 1)` 可得左邊數來第一個位元為 1 的索引,索引即是 $\log_2 v$ 取整結果
#### 在 linux 核心相關實作
在 [linux/log2.h](https://github.com/torvalds/linux/blob/master/include/linux/log2.h) 中可看到相關程式碼
#### `__ilog2_u32` 與 `__ilog2_u64`
> non-constant log of base 2 calculators
`fls` 定義在 [linux/include/asm-generic/bitops/fls.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/bitops/fls.h) 為 `generic_fls` 的巨集
以下是 `generic_fls` 註解,可以看到返回的 most-significant bit set 位置是由 1 開始作計算
/**
* generic_fls - find last (most-significant) bit set
* @x: the word to search
*
* This is defined the same way as ffs.
* Note fls(0) = 0, fls(1) = 1, fls(0x80000000) = 32.
*/
而因為對數取底是由 0 開始,因此求得的 `fls(n)` 要減去 1
```c
#ifndef CONFIG_ARCH_HAS_ILOG2_U32
static __always_inline __attribute__((const))
int __ilog2_u32(u32 n)
{
return fls(n) - 1;
}
#endif
```
**疑惑**
:::info
generic_fls 為何不要直接用 fls 來命名就好 ?
:::
#### `const_ilog2` :
常數版本的 `ilog2`
**疑惑**
:::success
原始碼是直接列出所有可能情況來找到最高 set bit
我認為是有特殊理由才會這樣處理,但我目前尚未找到解釋
最後看到老師提供的[解釋](https://www.facebook.com/groups/system.software2024/permalink/1575264409918118/),主要是編譯器可透過 CSE 一類的最佳化手法來消除該複雜敘述
:::
#### 應用案例
[linux/lib/math/div64.c](https://github.com/torvalds/linux/blob/486291a0e6246364936df1ecd64c90affef4b9c5/lib/math/div64.c#L193) 中的 `mul_u64_u64_div_u64`
該函式有三個 u64 型別的變數 (a, b, c),函數功能即計算 a * b / c
>u64 根據 [linux/arch/powerpc/boot/types.h](https://github.com/torvalds/linux/blob/master/arch/powerpc/boot/types.h#L12) 實際上為 unsigned long long
因為 a * b 有可能會溢位,函式有作對應的處理,條件判斷就有用到 `ilog2`
/* can a * b overflow ? */
if (ilog2(a) + ilog2(b) > 62)
> ilog2(a) = 60 $\rightarrow$ a 數值可用 61 個位元表示
> ilog2(b) = 3 $\rightarrow$ b 數值可用 4 個位元表示
> a * b 最多會需要 65 位元來表示
> 因此上述程式碼才會用大於 62 當作是否會溢位的標準
### [測驗 `5`](https://hackmd.io/@sysprog/linux2024-quiz3#%E6%B8%AC%E9%A9%97-5)
#### 解釋運作原理
`r |= shift` 的功用與 `ilog2` 的 `result` 一樣,都是去紀錄最高 set bit 所在索引
過程中 `r` 與 `shift` 皆為2的冪次,因此兩者相加可用 `|` 實現
回傳值 `x > 1` 是為了應對 x 是否等於 0x2 的情況,是則必須將原先得到的結果 `r |= shift` 再加 1
`x--` 則是用來應對 x 為2的冪次情況,與回傳值後面的加 1 相抵銷,來使結果是正確的
#### 改進程式碼
對 0 取對數沒有定義,因此我讓函式遇到 0 時會回傳 -1
對 1 取對數要是為 0 ,但原先程式碼會回傳 1 , 因此我進行改寫來作正確的處理
增加兩個變數
* `is_zero` : 判斷 `x` 是否為 0
* `gt_one` : 判斷 `x` 是否大於 1
將 `x--`,修正成 `x -= !!x`,避免 x = 0 時的溢位狀況
`(r | shift | x > 1)` 在 x = 0 , x = 1 時為 0,加入兩個表達式 : `(1 & gt_one)` 與 `(1 & gt_one)`
前者會確保當 `x` = 1 時,回傳為 0,後者確保當 `x` = 0 時,回傳為 -1
```diff
int ceil_ilog2(uint32_t x)
{
uint32_t r, shift;
+ bool is_zero, gt_one;
+ is_zero = !x;
+ gt_one = x > 0b1;
- x--;
+ x -= !!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 > GGG) + 1;
+ return (r | shift | x > 1) + (-is_zero) + (1 & gt_one);
}
```
## 第四週測驗題
### [測驗 `1`](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-1)
#### 針對 [LeetCode 477. Total Hamming Distance][(https://](https://leetcode.com/problems/total-hamming-distance/description/)) 撰寫出高效程式碼
用 `totalHammingDistance` 來解這題的話,需兩兩配對計算 hamming distance,複雜度為 $\mathcal{O}(n^2)$
```graphviz
digraph G {
node [shape=plaintext];
rankdir=LR;
Table [
label=<
<table border="1" cellborder="1" cellspacing="0">
<tr><td colspan="7">Sample Table</td></tr>
<tr>
<td>n'th bits</td>
<td>3</td>
<td>2</td>
<td>1</td>
<td>0</td>
</tr>
<tr><td>input 7</td><td>0</td><td>1</td><td>1</td><td>1</td></tr>
<tr><td>input 5</td><td>0</td><td>1</td><td>0</td><td>1</td></tr>
<tr><td>input 10</td><td>1</td><td>0</td><td>1</td><td>0</td></tr>
<tr><td>input 15</td><td>1</td><td>1</td><td>1</td><td>1</td></tr>
</table>
>,
];
}
```
以上面表格而言,共有 4 個輸入 (7, 5, 10, 15)
考慮所有輸入的第三個位元
>位元為 1 的有 3 個,為 0 的有 1 個,那麼該位置的 Hamming Distance 加總就為 3 * 1 = 3
考慮所有輸入的第二個位元
>位元為 1 的有 2 個,為 0 的有 2 個,那麼該位置的 Hamming Distance 加總就為 2 * 2 = 4
由此規律先計算每個位元 0 和 1 的個數,再進行相乘,再把結果紀錄在Total Hamming Distance
#### 實作 (版本1)
透過 `ones` 陣列儲存對所有輸入,在第 i 個位元為 1 的加總
最後跑過整個 `ones` ,透過 `ans += (numsSize - ones[i]) * ones[i]` 算出答案
```c
int totalHammingDistance(int* nums, int numsSize) {
int ans = 0, ones[32] = {0};
for(int i=0 ; i<numsSize ; i++){
int num = nums[i], index = 0;
while(num){
ones[index] += num & 0b1;
num = num >> 1;
++index;
}
}
for(int i=0 ; i<32 ; i++)
// ((numsSize - zero[i])) the count of "0"
ans += (numsSize - ones[i]) * ones[i];
return ans;
}
```
#### (版本2)
用兩層迴圈從第一個位元開始檢查,一累加完個數就更新 `ans`
此作法不用額外空間,空間複雜度為 $\mathcal{O}(1)$
```c
int totalHammingDistance(int* nums, int numsSize) {
int ans = 0;
for(int i=0 ; i<32 ; i++){
int ones = 0;
for(int j=0 ; j<numsSize ; j++)
ones += nums[j] >> i & 0b1;
// (numsSize - ones) the count of "0"
ans += (numsSize - ones) * ones;
}
return ans;
}
```
兩種做法皆會對每個輸入的所有位元跑過一次,而 int 為 32 bits
假設 n 為輸入筆數,因此迴圈執行次數最多是 32 * n,複雜度為 $\mathcal{O}(n)$