# 2023q1 第 2 週測驗題 contributed by <[`tintinjian12999`](https://github.com/tintinjian12999)> #### 題目請參考 [2023q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz2) ## 測驗一 ### 解釋程式碼的運作原理 考慮題目所給的程式碼 ```clike 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; } ``` 假設 `x` 初始為 `x = 010...000` (只有第二個 bit 為1,其餘63個bit皆為0) 在經過前七行的 `x |= x >> 1` 後, `x` 變為 `x = 011111111...000` ,此時為了增加填補1的效率可以一次將 `x` 與自身向右位移8個bit的結果做 bit-wise or ,由此得到的結果為 `x = 01111111111111111...000` ,因此 AAAA 為8,並依此類推將得到的結果再往右16 bit (因此 BBBB 的答案為16)與32 bit 做 bit-wise or 就能得到指定位數以下全部填補為1的結果,最後只要將 x 加 1 就能得到所要求的結果,因此 CCCC 為x + 1。 ### 用 __builtin_clzl 改寫 從[Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)裡的敘述 ``` Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. ``` 可以知道 clzl 的作用是找出 x 裡最高位1左側為0的位元數,因此若要找出最接近且大於等於 2 的冪的值只需要將0x8000000000000000右移為0的位元數 - 1個單位即可 ```clike uint64_t next_pow2(uint64_t x) { int number = __builtin_clzl(x); return 0x8000000000000000 >> (number - 1); } ``` ### 對應的 x86 指令 ```clike .file "next_pow2.c" .text .p2align 4 .globl next_pow2 .type next_pow2, @function next_pow2: .LFB0: .cfi_startproc endbr64 movabsq $-9223372036854775808, %rax bsrq %rdi, %rcx xorq $63, %rcx subl $1, %ecx shrq %cl, %rax ret .cfi_endproc .LFE0: .size next_pow2, .-next_pow2 .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: ``` > 延伸問題 > * 在 Linux 核心原始程式碼找出類似的使用案例並解釋 ## 測驗二 ### 解釋程式碼的運作原理 考慮下述的程式碼 ```clike int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; /* the bit length to be shifted */ /* use long here as it potentially could overflow for int */ long ans = 0; 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; } return ans; } ``` 我們知道若 i 為2的次方時則用來表達該數的 bit 需要加1 (2 = $10_2$, 4 = $100_2$),這裡的 ```clike if (!(DDDD)) len++; ``` 就是在做這件事情,由註解可以得知要判斷是否為2的次方只需要將最右邊的1移除後再觀察是否為0即可,若為0則代表為2的次方(2的次方在二進制下只會有一個1)。將最右邊的1移除可以透過對該數與自身 - 1的結果做 bit-wise AND 達成,因此這裡的 DDDD 為 `i &= (i - 1)`。 接下來考慮 ```clike ans = (i | (EEEE)) % M; ``` 我們知道模除運算是具有分配律的,因此在分析的過程中可以無視 `% M`的部分 (因為這裡的迴圈不會對模除的結果造成影響),以下敘述的 `ans_bef` 是 `ans` 在做模除運算前的結果。 假設今天考慮 `n = 4` 的情形,當 `i = 1` 時 `ans_bef = 1`,當 `i = 2` 時 `ans_bef` 須為 `ans_bef = 110` ,可以看到需要將 `1` 往左移動兩個 bit 並將 `2` set 進去,同理 `i = 3` 時 `ans_bef = 11011`,當 `i = 4` 時 `ans_bef` 須為 `ans_bef = 11011100` ,需要將 `11011` 向左移動三個 bit 再將 `3` set 進去,由此可知 EEEE 的答案為 `i << len`。 > 延伸問題 > * 嘗試使用 __builtin_{clz,ctz,ffs} 改寫,並改進模除運算 ## 測驗三 ### 解釋程式碼的運作原理 ## 測驗四 ### 解釋程式碼的運作原理 透過觀察前四個符合程式碼的樣式(0x8000, 0xc000, 0xe000, 0xf000)可以發現,此段程式碼檢查的是1從最高位元開始重複,直到數值出現0之後接下來的 bit 皆需為0,如0b1100滿足樣式而0b1001則不滿足。 若只考慮4個bit,假設符合樣式的數值為 x ``` x = 0b1000 x = 0b1100 x = 0b1110 x = 0b1111 ``` 則 ``` ~x = 0b0111 ~x = 0b0011 ~x = 0b0001 ~x = 0b0000 ``` ``` ~x + 1 = 0b1000 ~x + 1= 0b0100 ~x + 1 = 0b0010 ~x + 1 = 0b0001 ``` ``` (~x + 1) ^ x = 0b0000 (~x + 1) ^ x = 0b1000 (~x + 1) ^ x = 0b1100 (~x + 1) ^ x = 0b1110 ``` 可以發現上述行為實際上是將x最右邊的 bit 刪除,此行為可以保證得到的結果必定小於原始的 x ,因此 EEEE = ~x + 1, FFFF 為 x 。 > 若數值不滿足樣式,則此結果不成立。 > 延伸問題 > * 在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇 參見 Data Structures in the Linux Kernel