# 2023q1 Homework2 (quiz2)
contributed by < `paulpeng-popo` >
> [題目](https://hackmd.io/@sysprog/linux2023-quiz2)
## 測驗一
### 解釋程式碼原理
輸入一個數,目標是找到最接近且大於等於 2 的冪的值,舉例來說
- $next\_pow2(7) = 8$
- $next\_pow2(13) = 16$
- $next\_pow2(42) = 64$
最初的想法,就是找出最左邊的 1,然後讓它左移 1 就完成了,程式如下:
```c
uint64_t my_next_pow2(uint64_t x) {
x |= x >> 32;
x |= x >> 16;
x |= x >> 8;
x |= x >> 4;
x |= x >> 2;
x |= x >> 1;
return (x ^ (x >> 1)) << 1;
}
```
運作如下,利用二分法的概念,逐步設定 (set 1) 每一部份對應的位置
```c
x = 0010000000000000 // input x
x = 00100000 00100000 // x |= x >> 8;
x = 0010 0010 0010 0010 // x |= x >> 4;
x = 00 10 10 10 10 10 10 10 // x |= x >> 2;
x = 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 // x |= x >> 1;
```
當然,細看最後一行 return,會發現有點冗長,最後經過 [JoshuaLee0321](https://hackmd.io/@p4uHLG53RQ2mdlxeWiipWg/quiz2#%E6%B8%AC%E9%A9%97%E4%B8%80) 提醒用 `x + 1` 其實就能取代 `(x ^ (x >> 1)) << 1`
接著回到老師給出的 snippets
```c
uint64_t next_pow2(uint64_t x)
{
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> AAAA;
x |= x >> 16;
x |= x >> BBBB;
return CCCC;
}
```
可以發現與我先前的程式相似,只是 bottom-up 和 top-down 的差別,因此也能進一步把前 7 次 rshift 1 合併成
```c
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
```
那這邊我發現一個小問題,就是雖然我們的目的是找出**最接近且大於等於 2 的冪的值**,但若輸入本身就是 2 的冪,那應該是要回傳 **自己** 還是 **下一個冪** ?
```c
uint64_t next_pow2(uint64_t x)
{
uint8_t lo = 0, hi = 63;
while (lo < hi) {
uint8_t test = (lo + hi)/2;
if (x < pow2(test)) { hi = test; }
else if (pow2(test) < x) { lo = test+1; }
else { return pow2(test); }
}
return pow2(lo);
}
```
我拿 `next_pow2` 跟 bitwise 的 `next_pow2` 比較
```c
next_pow2(64) // output = 64
// bitwise
next_pow2(64) // output = 128
```
結果沒錯,兩個函式並不等價
根據我在 Linux Kernel 看到 next_pow2 的用法,我認為應該 2 的冪應該要回傳 **自己**
因此將 bitwise 的 `next_pow2` 修改成如下:
```c
uint64_t y = x;
...
return y == ((x+1) >> 1) && y ? y : x + 1;
```
這樣就可以回傳本來是冪次的 x
參考 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-quiz2#%E7%A8%8B%E5%BC%8F%E8%AA%AA%E6%98%8E) 的程式說明後,發現有較簡單的作法,也就是先將輸入的 x 減 1 後,再去做 set,這樣就可以讓最後的結果維持一樣
```c
x--;
...
return x + 1;
```
### 用 __builtin_clzl 改寫
```c
int __builtin_clz (unsigned int x)
int __builtin_clzl (unsigned long)
```
> Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
`__builtin_clzl` 會回傳最左邊的 1 其前面有幾個 0,因此我只要把 1 左移 64 - n 個 bit 就可以得到答案了
同時也注意到 `__builtin_clzl` 可能會有 undefined behavior,所以我們要先處理 `x = 0` 的情況
```c
uint64_t next_pow2(uint64_t x)
{
if (!x)
return 1;
if (!(x & (x - 1)))
return x;
return 1 << (64 - __builtin_clzl(x));
}
```
### Linux 核心原始程式碼 類似的使用案例
在 Dec 17 2014 之前的 commits,我們可以在 [util.h](https://github.com/torvalds/linux/commit/91529834d1dea9afccb72843c3e547e703ec177f) 找到 `next_pow2` 的 definition
這裡的 `next_pow2_l` 使用條件編譯的方式增加了對 unsigned long 的支援
```c
static inline unsigned next_pow2(unsigned x)
{
if (!x)
return 1;
return 1ULL << (32 - __builtin_clz(x - 1));
}
static inline unsigned long next_pow2_l(unsigned long x)
{
#if BITS_PER_LONG == 64
if (x <= (1UL << 31))
return next_pow2(x);
return (unsigned long)next_pow2(x >> 32) << 32;
#else
return next_pow2(x);
#endif
}
```
而在之後,這兩個函式被 [roundup_pow_of_two](https://github.com/torvalds/linux/commit/bd1857948e7667313f9a7bee9b2492c0848174a6) 取代,其中有提到 `fls_long` 是用來找從 MSB 開始出現第一個 1 的位置,而 `fls_long` 的實作中也可以見到 `clz` 的身影
[`linux/include/linux/bitops.h`](https://github.com/torvalds/linux/blob/master/include/linux/bitops.h#L203)
額外參考資料:[linux2021 彙整學員們的作業成果](https://hackmd.io/@gyes00205/HytYnbKtu#The-Linux-Kernel-API)
```c=
static inline __attribute__((const))
unsigned long __roundup_pow_of_two(unsigned long n)
{
return 1UL << fls_long(n - 1);
}
#define roundup_pow_of_two(n) \
( \
__builtin_constant_p(n) ? ( \
(n == 1) ? 1 : \
(1UL << (ilog2((n) - 1) + 1)) \
) : \
__roundup_pow_of_two(n) \
)
```
注意到,`roundup_pow_of_two` 在第 10 行多做一層判斷,也就是當 n = 1 時,讓值直接為 1,而不繼續呼叫 `ilog2((n) - 1)` 來處理,猜測原因除了減少函式執行的 clock cycles 數外,也避免了 `__builtin_clzll` 輸入為 0 的情況
```c
#define ilog2(n) \
( \
__builtin_constant_p(n) ? \
((n) < 2 ? 0 : \
63 - __builtin_clzll(n)) : \
(sizeof(n) <= 4) ? \
__ilog2_u32(n) : \
__ilog2_u64(n) \
)
```
在應用方面比如說 [drivers/md/raid5.c](https://github.com/torvalds/linux/blob/master/drivers/md/raid5.c#L6972) 中的 `raid5_store_stripe_size`,他在裡面就有檢查 new 是否為 2 的冪次方大小
```c
if (new % DEFAULT_STRIPE_SIZE != 0 ||
new > PAGE_SIZE || new == 0 ||
new != roundup_pow_of_two(new))
return -EINVAL;
```
引用 chatgpt 的解釋
> raid5_store_stripe_size() 是 Linux 核心中用於處理 RAID5 塊裝置儲存條帶大小的函式。RAID5 是一種磁碟陣列技術,透過將資料條帶化(striping)儲存在多個磁碟中,並使用奇偶校驗(parity)實現資料冗餘和容錯。
>
> 該函式的作用是將使用者傳入的儲存條帶大小值(stripe_size)儲存在 RAID5 塊裝置相關的結構體中,從而影響磁碟陣列的讀寫效能和空間利用效率。具體來說,該函式會檢查儲存條帶大小值是否符合一定的規範,例如必須是 2 的冪次方,且不能小於硬體支援的最小條帶大小等。如果輸入的值不符合要求,函式將返回錯誤碼,否則將更新 RAID5 塊裝置結構體中的儲存條帶大小資訊。
### 編譯器能否產生對應的 x86 指令
```shell
$ gcc -O2 -std=c99 -S test1.c
```
可以看到第 12 行有 [`bsr`](https://www.felixcloutier.com/x86/bsr#description) 指令出現
```=
next_pow2:
.LFB26:
.cfi_startproc
endbr64
movl $1, %eax
testq %rdi, %rdi
je .L10
leaq -1(%rdi), %rdx
movq %rdi, %rax
testq %rdi, %rdx
je .L10
bsrq %rdi, %rax
movl $64, %ecx
xorq $63, %rax
subl %eax, %ecx
movl $1, %eax
sall %cl, %eax
cltq
.L10:
ret
.cfi_endproc
```
## 測驗二
### 解釋程式碼運作原理
[LeetCode 1680. Concatenation of Consecutive Binary Numbers](https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/)
給定一整數 n,回傳將 1 到 n 的二進位表示法依序串接在一起所得到的二進位字串,其所代表的十進位數字 mod $10^9 + 7$ 之值。
- 想法:
- 每次左移 n 個 bit(s)
- 然後跟要 concate 的數字做 OR operation
舉例:concate 1, 2, 3
```
|
v
0000 0000 // 初始值
|
v
0000 0000 // 左移 n bit(s),這時 n = 1
0000 0001 // 跟 1 做 OR
|
v
0000 0100 // 左移 n bit(s),這時 n = 2
0000 0110 // 跟 2 做 OR
|
v
0001 1000 // 左移 n bit(s),這時 n = 2
0001 1011 // 跟 3 做 OR
輸出 27
```
- 該怎麼決定 n
- 可以發現,當要 concate 2 的冪的時候,n 會比前一次多位移 1
- 而非 2 的冪的時候,n 維持前一次的值即可
回到老師給出的 snippets
```c=
for (int i = 1; i <= n; i++) {
/* removing the rightmost set bit
* e.g. 100100 -> 100000
* 000001 -> 000000
* 000000 -> 000000
* after removal, if it is 0, then it means it is power of 2
* as all power of 2 only contains 1 set bit
* if it is power of 2, we increase the bit length
*/
if (!(DDDD))
len++;
ans = (i | (EEEE)) % M;
}
```
可以看到第 10 行用來判斷 i 是否為 2 的冪,是的話,長度 len 加 1
而 12 行用來做位移然後相加,最後處理 mod $10^9 + 7$
### 使用 \_\_builtin_{clz,ctz,ffs} 改寫
```c
int __builtin_ffs (int x);
int __builtin_clz (unsigned int x);
int __builtin_ctz (unsigned int x);
```
> /\* __builtin_ffs(x) \*/
> Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero.
>
> /\* __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.
>
> /\* __builtin_ctz(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.
找出 i 前面連續 0 的個數與後面連續 0 的個數相加是否為 31,如果為 31 代表 i 為 2 的冪次方 ; 接著使用 `__builtin_ffs` 找到需要位移的長度
```c
for (int i = 1; i <= n; i++) {
if (__builtin_clz(i) + __builtin_ctz(i) == 31)
len = __builtin_ffs(i);
ans = (i | (ans << len)) % M;
}
```
參考 [joshualee0321](https://hackmd.io/@p4uHLG53RQ2mdlxeWiipWg/linux-2023-quiz2#%E4%BD%BF%E7%94%A8__builtin-%E6%94%B9%E5%AF%AB) 的作法,發現可以只使用 `__builtin_clz` 便直接得出 len 的大小,同時也減少使用 branch 的開銷
```c
for (int i = 1; i <= n; i++) {
len = 32 - __builtin_clz(i);
ans = (i | (ans << len)) % M;
}
```
### 改進 mod $10^9 + 7$ 的運算
在上述程式碼中,`i | (ans << len)` 的結果有可能在做 modulo M 之前就已經 overflow 了,雖然可能不會造成計算上的錯誤,但讓程式存在 overflow 風險並不是一件好事,因此根據 modulo 的性質
- $(A + B) \% M = (A \% M + B \% M) \% M$
- $(A \ast B) \% M = (A \% M \ast B \% M) \% M$
可以將程式碼改成如下:
```c
ans = ((i % M) | ((ans % M) << (len % M))) % M;
```
## 測驗三
### 解釋程式碼原理
```c
size_t count_utf8(const char *buf, size_t len)
{
const int8_t *p = (const int8_t *) buf;
size_t counter = 0;
for (size_t i = 0; i < len; i++) {
/* -65 is 0b10111111, anything larger in two-complement's should start
* new code point.
*/
if (p[i] > -65)
counter++;
}
return counter;
}
```
(待解釋)
```c
size_t swar_count_utf8(const char *buf, size_t len)
{
const uint64_t *qword = (const uint64_t *) buf;
const uint64_t *end = qword + len >> 3;
size_t count = 0;
for (; qword != end; qword++) {
const uint64_t t0 = *qword;
const uint64_t t1 = ~t0;
const uint64_t t2 = t1 & 0x04040404040404040llu;
const uint64_t t3 = t2 + t2;
const uint64_t t4 = t0 & t3;
count += __builtin_popcountll(t4);
}
count = (1 << 3) * (len / 8) - count;
count += (len & 7) ? count_utf8((const char *) end, len & 7) : 0;
return count;
}
```
(待解釋)
### 比較 SWAR 和原本的實作效能落差
可以使用 perf 比較效能
(待解釋)
### 在 Linux 核心原始程式碼找出 UTF-8 和 Unicode 相關字串處理的程式碼,探討其原理,並指出可能的改進空間
可以在 [unicode.c](https://github.com/torvalds/linux/blob/master/fs/udf/unicode.c#L46) 中找到有關 unicode 相關的字串處理程式碼
```c
static unicode_t get_utf16_char(const uint8_t *str_i, int str_i_max_len,
int str_i_idx, int u_ch, unicode_t *ret)
{
unicode_t c;
int start_idx = str_i_idx;
/* Expand OSTA compressed Unicode to Unicode */
c = str_i[str_i_idx++];
if (u_ch > 1)
c = (c << 8) | str_i[str_i_idx++];
if ((c & SURROGATE_MASK) == SURROGATE_PAIR) {
unicode_t next;
/* Trailing surrogate char */
if (str_i_idx >= str_i_max_len) {
c = UNICODE_MAX + 1;
goto out;
}
/* Low surrogate must follow the high one... */
if (c & SURROGATE_LOW) {
c = UNICODE_MAX + 1;
goto out;
}
WARN_ON_ONCE(u_ch != 2);
next = str_i[str_i_idx++] << 8;
next |= str_i[str_i_idx++];
if ((next & SURROGATE_MASK) != SURROGATE_PAIR ||
!(next & SURROGATE_LOW)) {
c = UNICODE_MAX + 1;
goto out;
}
c = PLANE_SIZE +
((c & SURROGATE_CHAR_MASK) << SURROGATE_CHAR_BITS) +
(next & SURROGATE_CHAR_MASK);
}
out:
*ret = c;
return str_i_idx - start_idx;
}
```
## 測驗四
### 解釋程式碼原理
首先觀察老師給出的範例
```c
for (; x > 0; x <<= 1) {
if (!(x & 0x8000))
return false;
}
return true;
```
- 什麼時候 return false
- 當 ($x$ & 0x8000) 為 0 時
- $x$ 為 0xxx ... xxxx (binary format 開頭為 0)
- 其中 X 不全為 0,否則跳出迴圈
- 什麼時候 return true
- 若 $x$ 跳出迴圈且為 0,則 return true
- for 迴圈作用
- 將 $x$ 往左移 1 位,但因為 $x$ 為 `uint16_t`,因此作用相當於在 LSB 補一 0,而 MSB 捨去 1 個 bit
- 像是 `1100 0000 0000 0000` 的下一次 iteration 就變成 `1000 0000 0000 0000`
接著看到 [測驗-4](https://hackmd.io/@sysprog/linux2023-quiz2#%E6%B8%AC%E9%A9%97-4) 老師給出的 pattern 中,全部的數值的 binary format 其開頭皆為連續的 1-bit,而之後的 bits 皆為 0
這就是我們需要的 pattern
要如何精簡以上程式碼,老實說我真想不到,因此只能看圖說故事
```c
bool is_pattern(uint16_t x)
{
const uint16_t n = -x;
return (n ^ x) < x;
}
```
首先,-x 會被先後操作 NOT x 跟 +1,也就是 x 的 2 補數,在這個情況下,`x` 中的最靠 LSB 側的 1-bit 會維持 set,該 bit 左側皆會被作 NOT,如:
- x = `1111 1100 0000 0000` , -x = `0000 0100 0000 0000`
- x = `1111 0010 0010 0000` , -x = `0000 1101 1110 0000`
- x = `0000 0010 0000 0000` , -x = `1111 1110 0000 0000`
這邊發現,變成 pow of two 的 n,在跟 x 作 XOR 後,the set bit 左側的 bits 都會是 1,而 the set bit 及其右側的 bits 皆為 0,因此 x 總是會比 (n ^ x) 多 1
```c
the set bit
|
v
n = 0000 0100 0000 0000
x = 1111 1100 0000 0000
n ^ x = 1111 1000 0000 0000
```
同理,非 pow of two 的 n,在跟 x 作 XOR 後,the set bit 左側的 bits 都會是 1,這時後 x 總是會比 (n ^ x) 小
```c
the set bit
|
v
n = 0000 1101 1110 0000
x = 1111 0010 0010 0000
n ^ x = 1111 1111 1100 0000
the set bit
|
v
n = 1111 1110 0000 0000
x = 0000 0010 0000 0000
n ^ x = 1111 1100 0000 0000
```
因此根據以上解釋,我認為程式碼也可以修改如下:
直接判斷 n 是否為 pow of two
```c
int __builtin_popcount (unsigned int x);
```
> Returns the number of 1-bits in x.
```c
bool is_pattern(uint16_t x)
{
const uint16_t n = -x;
return __builtin_popcount(n) == 1;
}
```
或是確認連續 1 和連續 0 是否佔滿 16 個 bits
最後再對 0 作額外判斷
```c
bool is_pattern(uint16_t x)
{
return x ? (__builtin_clz(~x) + __builtin_ctz(x)) == 16 : 0;
}
```
### 在 Linux 核心原始程式碼找出上述 bitmask 及產生器
在 [bits.h](https://github.com/torvalds/linux/blob/master/include/linux/bits.h#L36) 可以找到 `GENMASK` macro 的定義
```c
/*
* Create a contiguous bitmask starting at bit position @l and ending at
* position @h. For example
* GENMASK_ULL(39, 21) gives us the 64bit vector 0x000000ffffe00000.
*/
#if !defined(__ASSEMBLY__)
#include <linux/build_bug.h>
#define GENMASK_INPUT_CHECK(h, l) \
(BUILD_BUG_ON_ZERO(__builtin_choose_expr( \
__is_constexpr((l) > (h)), (l) > (h), 0)))
#else
/*
* BUILD_BUG_ON_ZERO is not available in h files included from asm files,
* disable the input check if that is the case.
*/
#define GENMASK_INPUT_CHECK(h, l) 0
#endif
```
假設 BITS_PER_LONG = 32,則 GENMASK(6, 4) 運作如下
1. 首先利用 `GENMASK_INPUT_CHECK` 檢查 `h` 和 `l` 是否合法 (`h` $\ge$ `l`)
2. ~UL(0) = `1111 1111 1111 1111 1111 1111 1111 1111`
3. UL(1) << 4 = `0000 0000 0000 0000 0000 0000 0001 0000`
4. ~UL(0) - (UL(1) << 4) + 1 = `1111 1111 1111 1111 1111 1111 1111 0000`
5. ~UL(0) >> (BITS_PER_LONG - 1 - 6) = `0000 0000 0000 0000 0000 0000 0111 1111`
6. 最後,將 4 和 5 的結果做 AND $\Rightarrow$ `0000 0000 0000 0000 0000 0000 0111 0000`
```c
#define __GENMASK(h, l) \
(((~UL(0)) - (UL(1) << (l)) + 1) & \
(~UL(0) >> (BITS_PER_LONG - 1 - (h))))
#define GENMASK(h, l) \
(GENMASK_INPUT_CHECK(h, l) + __GENMASK(h, l))
```