# 2023q1 Homework2 (quiz2) contributed by < [ctc2324](https://github.com/ctc2324) > ## 測驗 1 ### 題目程式碼 ```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 >> 1; x |= x >> 16; x |= x >> 32; return (x+1); } ``` ### 延伸問題 1. 解釋上述程式碼原理,並用 `__builtin_clzl` 改寫 1. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 1. 當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 `x86` 指令? #### 1. 解釋上述程式碼原理,並用 `__builtin_clzl` 改寫 ##### 程式碼原理 以上程式目標為針對給定的無號64位元數值 `x` ,找出最接近且大於等於 `2` 的冪的值。 因此我們只要將此數最大位元之後的位元全數填滿 `1` ,再將最後 `return` 值加上1,就可以得到最接近 `x` 的冪值。 此程式透過將 `x` 位元移動,並與 `x` 做 OR 運算,來將後面位元填滿 1 。 因此可以知道在64位元的清況下,最多只需要操作63次即可達成。已知每次操作都會以 2 的倍數成長,因此可以將程式碼優化為: ```c uint64_t next_pow2(uint64_t x) { x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x |= x >> 32; return (x+1); } ``` 也可以達成一樣的效果。 以下舉例 `x` 為 `0x30000000` ,其在二進位表示下為 `0011 0000 0000 0000 0000 0000 0000 0000` ```c uint64_t next_pow2(uint64_t x) { // x = 0011 0000 0000 0000 0000 0000 0000 0000 x |= x >> 1; // x = 0011 1100 0000 0000 0000 0000 0000 0000 x |= x >> 2; // x = 0011 1111 0000 0000 0000 0000 0000 0000 x |= x >> 4; // x = 0011 1111 1111 0000 0000 0000 0000 0000 x |= x >> 8; // x = 0011 1111 1111 1111 1111 0000 0000 0000 x |= x >> 16; // x = 0011 1111 1111 1111 1111 1111 1111 1111 x |= x >> 32; // x = 0011 1111 1111 1111 1111 1111 1111 1111 return (x+1); // x = 0100 0000 0000 0000 0000 0000 0000 0000 } ``` 輸出為 `0100 0000 0000 0000 0000 0000 0000 0000` 即十進位的 `1073741824`。 ##### 用 `__builtin_clzl` 改寫 `__builtin_clzl` 函式定義為傳回輸入數 `x` 最高位開始連續的 0 的個數。以上面的 `0x30000000` 為例: :::info x = 0011 0000 0000 0000 0000 0000 0000 0000 最高位數字為從左邊數來的第35位元,因此 `__builtin_clzl` 函式會回傳34,表示在最高位元左邊有連續34個0。 ::: 了解 `__builtin_clzl` 函式的作用後,我們便可以將程式改寫成: ```c uint64_t next_pow2(uint64_t x) { if(x==0) return 1; int clz; clz = __builtin_clzl(x); return 1 << (64-clz); } ``` 此處須注意在 `__builtin_clzl` 中提及若傳入值為 0 是未定義的情況,因此若傳入值為 0 則直接回傳 1 。 #### 3.當上述 `clz` 內建函式已運用時,編譯器能否產生對應的 `x86` 指令? 在linux系統編輯 `test.c` 透過gcc確定可編譯後,執行 `cc -O2 -std=c99 -S test.c` ,此時會產生出test.s檔,打開便可看到對應的 x86 指令如下: ``` next_pow2: .LFB13: .cfi_startproc endbr64 movl $1, %eax testq %rdi, %rdi je .L1 bsrq %rdi, %rdi movl $64, %ecx xorq $63, %rdi subl %edi, %ecx sall %cl, %eax cltq ``` :::warning 1. 在 Linux 核心原始程式碼找出類似的使用案例並解釋 ::: ## 測驗 2 ### 題目程式碼 ```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 (!(i & (i-1))) len++; ans = (i | (ans << len)) % M; } return ans; } ``` ### 延伸問題 1. 解釋上述程式碼運作原理 1. 嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫,並改進 mod $10^9+7$ 的運算 #### 1.解釋上述程式碼運作原理 本程式碼目標為給定一整數 $n$ ,回傳將 1 到 $n$ 的二進位表示法依序串接在一起所得到的二進位字串,其所代表的十進位數字 mod $10^9+7$ 之值。 使用 `len` 紀錄數字的長度,由於若遇到 2 的冪值,2進位的數字長度會加 1 ,因此此處判斷 `i` 及 `i-1` 經過 `AND` 運算的值是否等於0。例如: :::info 該數非2的冪值: 5 -> 0101 4 -> 0100 `AND`運算後為0100 並非為0,則 5 非 2 的冪值。 該數為2的冪值: 4 -> 0100 4 -> 0011 `AND`運算後為0000 為0,則 4 為 2 的冪值。 ::: 因此若是 `AND` 運算後為0,此數即為 2 的冪值,將 `len` 值+1。 最後再將 `ans` 向左位移 `len` 數的位元並與 `i` 做 `OR` 運算,實現將數字的二進位串接在一起的功能。每次進行 `OR` 運算後再mod $10^9+7$。最後回傳 `ans` 值。 #### 2.嘗試使用 `__builtin_{clz,ctz,ffs}` 改寫,並改進 mod $10^9+7$ 的運算 ```c int concatenatedBinary(int n) { const int M = 1e9 + 7; int len = 0; long ans = 0; for (int i = 1; i <= n; i++) { len = 64 - __builtin_clzl(i); ans = (i | (ans << len)) % M; } return ans; } ``` 使用第一題使用過的 `__builtin_clzl` 函式來獲得輸入值 `n` 的二進位最高位元位置,代表整串二進位長度,其他與題目程式碼無異。 :::warning 改進 mod $10^9+7$ 的運算 ::: ## 測驗 3 ### 題目程式碼 ```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; } ``` ### 延伸問題 1. 解釋上述程式碼運作原理,比較 SWAR 和原本的實作效能落差 1. 在 Linux 核心原始程式碼找出 UTF-8 和 Unicode 相關字串處理的程式碼,探討其原理,並指出可能的改進空間 #### 1.解釋上述程式碼運作原理,比較 SWAR 和原本的實作效能落差 本程式透過 SWAR 技巧來計算輸入一 UTF-8 字串中的Unicode數量。 首先將傳入的 `buf` 轉換成 `uint64_t` 的形式,並且設變數 `end` 指向 64 bits 中的最後一位數,以便接下來的迴圈有終止點。 接下來設定迴圈,一次8個 bytes 來做檢查。目標為找出最高2個位元設定為 `10` 的**後續位元組**。 ```c 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); } ``` 1.此處與原題目相同,假設 `t0` 為: ```c t0 = [00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx] ^^^^^^^^^ *continuation bytes* ``` 2.反轉位元: ```c t1 = ~t0; t0 = [00xx.xxxx|01xx.xxxx|10xx.xxxx|11xx.xxxx] t1 = [11xx.xxxx|10xx.xxxx|01xx.xxxx|00xx.xxxx] ``` 3.透過與 0x40404040 進行 `AND` 運算來隔離反向的第六個位元。 ```c t2 = t1 & 0x40404040; t2 = [0100.0000|0000.0000|0100.0000|0000.0000] ``` 4.對第 6 個位元,向左位移 1 個位元 (移至第 7 個位元的位置) ```c t3 = t2 +t2; t3 = [1000.0000|0000.0000|1000.0000|0000.0000] ``` 5.進行 `not bit6 and bit7` ```c t4 = t0 & t3; t4 = [0000.0000|0000.0000|1000.0000|0000.0000] ``` 此時可以發現,只剩下第三個位元組有數字。 6.使用函式 `__builtin_popcountll` 來計算1的數量。 ```c count += __builtin_popcountll(t4); count = 1; ``` 此時計數器 count 即為後續位元組的數量。 ## 測驗 4 ### 題目程式碼 ```c bool is_pattern(uint16_t x) { const uint16_t n = -x; return (n ^ x) < x; } ``` ### 延伸問題 1. 解釋上述程式碼運作原理 1. 在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇 #### 1.解釋上述程式碼運作原理 此程式碼目標為判別目標數在二進位下是否符合某些條件。 ```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; } ``` 從題目第一個程式碼中可以推測出,條件為判斷此二進位是否符合 "從LSB 到RSB 中間是否都為 1" 舉例來說: :::info 十六進位 -> 二進位 符合: 8000 -> 1000000000000000(從 LSB 至 RSB 皆為 1) FFF0 -> 1111111111110000(從 LSB 至 RSB 皆為 1) 不符合: 8001 -> 1000000000000001 ::: 因此精簡後的程式先設16位元變數 `n` 等於 `-x` ,也就是對 `x` 取負值,並且透過與原 `x` 進行 `XOR` 運算,若此數滿足以上條件,則其與原 `x` 做 `XOR` 運算其值必小於 `x` 。 舉例來說: 以 FFF0 (16進位)為例, `x = 1111111111110000` 取負號後即對二進位每個位元反轉後加一, `n = 0000000000010000` ,此數與原 `x` 進行 `XOR` 運算後為 `1111111111100000` ,此數比原本的 `x` 小,故 FFF0 為符合規定的數。 再舉 FFF1 (16進位)為例,`x = 1111111111110001` 取負號後即對二進位每個位元反轉後加一,`n = 0000000000001111` ,此數與原 `x` 進行 `XOR` 運算後為 `1111111111111110` ,此數比原本的 `x` 大,故 FFF1 為不符合規定的數。 #### 在 Linux 核心原始程式碼找出上述 bitmask 及產生器,探討應用範疇