# 2023q1 Homework2 (quiz2) contributed by < `sherrygottalent` > > [第 2 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz2) :::spoiler 開發環境 ```shell $ gcc --version gcc (Ubuntu 12.2.0-3ubuntu1) 12.2.0 $ lscpu Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Vendor ID: 0x00 Model name: - Model: 0 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 Stepping: 0x0 BogoMIPS: 48.00 ``` ::: :::spoiler ToDo - [ ] 測驗 1 - [x] 解釋上述程式碼原理,並用 `__builtin_clzl` 改寫 - [ ] 在 Linux 核心原始程式碼找出類似的使用案例並解釋 - [ ] 當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 x86 指令? - [ ] 測驗 2 - [x] 解釋上述程式碼運作原理 - [ ] 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫,並改進 mod $10^9+7$的運算 - [ ] 測驗 3 - [x] 測驗 4 ::: ## 測驗 1 :::spoiler 測驗內容 參照 你所不知道的 C 語言: bitwise 操作,考慮 next_pow2 可針對給定無號 64 位元數值 x,找出最接近且大於等於 2 的冪的值 ``` 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; } ``` 請補完程式碼,使其符合預期。作答規範: - AAAA 和 BBBB 皆為數值,且 AAAA 小於 BBBB - CCCC 為表示式 :::success 延伸問題: 1. 解釋上述程式碼原理,並用 `__builtin_clzl` 改寫 > int __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. > int __builtin_clzl (unsigned long) > Similar to `__builtin_clz`, except the argument type is unsigned long. 2. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 3. 當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 x86 指令? > 提示: 可執行 `cc -O2 -std=c99 -S next_pow2.c` 並觀察產生的 `next_pow2.s` 檔案,確認 `bsrq` 指令 (表示 “Bit Scan Reverse”) ::: ### 程式碼原理理解 對 64 位元數值 `x` 操作,逐續填補位元後,要得到最接近且大於等於 2 的冪的值 - step 1. 填補位元,從最高位元開始移動(`>`)填補(`|`), 從 `x` 有 1 的位元開始後,後面的位元都會是 1 前段程式處理 8 個位元,直覺上會覺得只處理前 8 個位元;實際是即使 `x` 數值為 1 的最高位元不在前 8 個位元,也會向右移動填補右側 8 個位元。 之後再移動 8 個位元,填補完 16 個位元, 逐續再移動 16 個位元,填補完 32 個位元, 移動 32 個位元,填補完 64 個位元,完成此步驟操作。 - step 2. 為了得到大於等於 2 的冪的值, 將 x+1 ,即為所求的結果 ### 答題 AAAA = 8 BBBB = 32 CCCC = x+1 ### [__builtin_clzl](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) 改寫 > int __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. `__builtin_clz` 回傳 `MSB` 方向連續位元 0 的長度,另 `len` 為 x 的位元總數, 則大於等於 2 的冪的值為 `1 << (len-__builtin_clz(x))` ``` c uint64_t next_pow2_clz(uint64_t x) { size_t len = sizeof(uint64_t) * 8; return 1 << (len-__builtin_clz((unsigned int)x)); } ``` `__builtin_clzl` > int __builtin_clzl (unsigned long) > Similar to `__builtin_clz`, except the argument type is unsigned long. ``` c uint64_t next_pow2_clzl(uint64_t x) { size_t len = sizeof(uint64_t) * 8; return 1 << (len-__builtin_clzl((unsigned long)x)); } ``` ### Linux 核心原始程式碼找出類似的使用案例 --- ## 測驗 2 :::spoiler 測試內容 LeetCode [1680. Concatenation of Consecutive Binary Numbers](https://leetcode.com/problems/concatenation-of-consecutive-binary-numbers/)給定一整數 $n$ ,回傳將 1 到 $n$ 的二進位表示法依序串接在一起所得到的二進位字串,其所代表的十進位數字 mod $10^9+7$ 之值。 ``` c 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; } ``` 請補完程式碼,使其符合預期。作答規範: - `DDDD` 和 `EEEE` 皆為表示式 :::success 延伸問題: 1. 解釋上述程式碼運作原理 2. 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫,並改進 mod $10^9+7$的運算 ::: ### 程式碼原理理解 將 10 進位數值轉換成 2 進位表示法後,相連,再從 2 進位轉換成 10 進位數值,回傳以 $10^9+7$ 為除數的餘數. - step 1 此處程式碼的目標是要取得 `len` 值,代表之後要向左移的次數 `len` 從初始值 0 開始慢慢增加,在碰到數值的二進位表述法位數增加時跟著增加 ``` if (!(DDDD)) len++; ``` 數值 i | 2 進位表示法| !(DDD) |len| ans ----|--------|-----|-----|--------- 1| $(1)_2$ | 1 | 1 (len++)| $(1)_2$ 2| $(10)_2$| 1 | 2 (len++)| $(110)_2$ 3| $(11)_2$| 0 | 2| $(11011)_2$ 4| $(100)_2$| 1 | 3 (len++)| $(11011100)_2$ 5| $(101)_2$| 0 | 3| $(11011100101)_2$ ...|||| 當數值 `i` 為 2 的冪時, len++ 此時數值有的特性, 1. 二進位表示法中只有一個bit為 `1` 2. 移除最右側位元後數值會是 0 (程式註解提示`removing the rightmost set bit`) :arrow_right: DDD = `i & (i - 1)` - step 2. ``` ans = (i | (EEEE)) % M; ``` 這個步驟要把 `i` 連接(`|`)到 `ans` 上,又已知位元移動數(`len`),EEE 應該將 `ans` 做適當的位元移動 :arrow_right: EEE = `ans << (len-1)` ### 答題 DDD = `i & (i - 1)` EEE = `ans << (len-1)` ### 改寫 :::info Built-in Function: int __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. Built-in Function: int __builtin_ctz (unsigned int 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. Built-in Function: int __builtin_ffs (int x) Returns one plus the index of the least significant 1-bit of x, or if x is zero, returns zero. ::: ``` c 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++) { if ((__builtin_clz(i) + __builtin_ctt(i) + 1 == sizeof(n) * 8) & (__builtin_ffs(i) == (1 << (len+1)) len++; ans = (i | (ans << (len-1))) % M; } return ans; } ``` ### 延伸疑問: Why mod $10^9+7$ ? overflow?還在想 :::warning 參照 [Barrett reduction](https://en.wikipedia.org/wiki/Barrett_reduction) :notes: jserv ::: --- ## 測驗 3 ### 程式碼原理理解 - SIMD(single Instruction, multiple data) within a register (SWAR) - :warning: 不是很了解一開始敘述兩數 concatenate 判斷是否為奇數的遮罩跟後續 SWAR 實作的關聯,只是說明 SWAR 是同時對多個位元操作的一種運算方法? - Unicode using one to four one-byte (8-bit) code units. - [SIMD and SWAR Techniques](https://www.chessprogramming.org/SIMD_and_SWAR_Techniques) > 若輸入的字串是一個有效的 UTF-8 序列,則計算其中的後續位元組數量,並將此數字從總字串長度中減去,即可確定字元數量。 > :question: code point是什麼? :::info Built-in Function: int __builtin_popcount (unsigned int x) Returns the number of 1-bits in x. ::: SWAR 實作如下: ``` 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++) { // 對一字元操作 qword = character 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; //取出qword 位元組 not bit6 and bit7 count += __builtin_popcountll(t4); // 看第六七位是否為 1 } count = (1 << AAAA) * (len / 8) - count; count += (len & BBBB) ? count_utf8((const char *) end, len & CCCC) : DDDD; return count; } ``` `buf` 型態是 pointer to char (string) `qword` 強制轉型為 64-bit 位元長度 `const uint64_t *end = qword + len >> 3;` 看不懂:warning: for-loop 對每個位元組(byte)操作 ### 作答 想法(猜測): - SWAR看起來是對稱的操作 - 提示: AAAA, BBBB, CCCC, DDDD 皆為常數 `count = (1 << AAAA) * (len / 8) - count;` :arrow_right: AAAA = 3 (因為後面有 / 8 )(**只是猜測:warning:**) --- ## 測驗 4 :::spoiler 測驗內容 以下程式碼可判定 16 位元無號整數是否符合特定樣式 (pattern): ``` c #include <stdint.h> #include <stdbool.h> bool is_pattern(uint16_t x) { if (!x) return 0; for (; x > 0; x <<= 1) { if (!(x & 0x8000)) return false; } return true; } ``` 符合上述程式碼的樣式,如下: ``` pattern: 8000 (32768) pattern: c000 (49152) pattern: e000 (57344) pattern: f000 (61440) pattern: f800 (63488) pattern: fc00 (64512) pattern: fe00 (65024) pattern: ff00 (65280) pattern: ff80 (65408) pattern: ffc0 (65472) pattern: ffe0 (65504) pattern: fff0 (65520) pattern: fff8 (65528) pattern: fffc (65532) pattern: fffe (65534) pattern: ffff (65535) ``` 改寫上述程式碼,使其達到等價行為,但更精簡有效。 ::: ### 程式碼原理理解 觀察可以通過的結果 MSB 都是 1,且數值如: 8、c、e、f 在二進位表示都是 MSB 後有連續位元 1 的表示法 可以知道檢驗的 pattern 是 MSB 為 1 ,且直到出現位元 0 之前連續為 1 不間斷的數值。 ### 答題 ``` bool is_pattern(uint16_t x) { const uint16_t n = EEEE; return (n ^ x) < FFFF; } ``` 參考 [YSRossi](https://hackmd.io/@YSRossi/quiz2-2023#%E6%B8%AC%E9%A9%97-4)重新思考, 線索: 1. `n^x` may toggle bits of 1 2. 因為 `(n ^ x) < FFFF`, `n^x` 運算後,不符合樣式的數值會在至少 MSB 後出現的第一個 0 轉換為 1 :::info e.g. x = $(9)_{10} = (1101)_2$, `x ^ n` may be $(1110)_2$,n = $(0011)_2$ = ~x+1 = $-x$ or `x ^ n` may be $(1111)_2$,n = $(0010)_2$ = ~x ::: 以 `~x` 測試可以找到失敗例子,所以 n = EEEE = `-x` or `~x+1` FFFF = x