--- tags: linux2022 --- # 2022q1 Homework3 (quiz3) contributed by < `YiChainLin` > > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz3) 實驗的 gcc 編譯器版本 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 ``` ## 測驗1 在 Linux 核心原始程式碼,[include/linux/bitfield.h](https://github.com/torvalds/linux/blob/master/include/linux/bitfield.h) 提及一個巨集 `GENMASK`,其作用是依據給定的範圍,產生連續的 [bitmask](https://en.wikipedia.org/wiki/Mask_(computing)),例如: * GENMASK(6, 4) 產生 01110000~2~ * GENMASK(39, 21) 產生 0x000000ffffe00000 (64 位元) 已知我們使用的微處理器架構為 64 位元,且 unsigned long 為 8 位元組寬度 (符合 LP64 資料模型),以下是可能的 GENMASK 巨集的定義: ```c #define GENMASK(h, l) \ (((~0UL) >> (LEFT)) & ((~0UL) >> (l) << (RIGHT))) ``` * 本題要根據示例要的結果是要產生在二進位元中,第 h + 1 (high) 位 ~ 第 l + 1 (low) 位的 `1` 遮罩 * 在無號整數下,使用[邏輯移位(logical shift)](https://en.wikipedia.org/wiki/Logical_shift) shift 移位補 0 的效果,完成本題 * 以範例來說我們預期的 `bitmask` 為: ``` GENMASK(39, 21): 0000...00111...111111000...000 (64位元) ^ ^ (第40位) (第22位) ``` 在 `((~0UL) >> (LEFT))` 敘述中要能產生最高位前面的 0,所以透過邏輯移位補 0 的特性中,要向右 shift 64 - (h + 1),64 位元是來自於題目的架構,如果是 32 位元的架構則 為 31 - h :::success LEFT = 63 - h ::: 所以對應這個敘述可以產生這樣的結果: ``` 000..00001111111...11111 (64位元) ^ (第40位) ``` 再來就是要將尾段清零,透過與 0 的 `&` 計算去除,所以透過向左移位補 0 的特性,可以補 `l` 個數的 0 所以在 `((~0UL) >> (l) << (RIGHT))` 的敘述中,透過右移補 0 的方式產生,先進行向右移位 `l` 次,再左移 `l`次 :::success RIGHT = l ::: ``` 右移 l: 再左移 l: 0000....00111...11111 (64位元) 111...1100...00 (64位元) ^ ^ (第 63 - l 位) (第 l 位) ``` 最後再將 high 的位移結果加入進行 `&` 運算 ``` 000..000011111111..11111 (64位元) & 111..1111111111100...000 ........................... 000..000011111110...0000 <-產生 (h + 1) ~ (l + 1)位的 1 bitmask ``` :::info 有一點比較不懂的是在 `(~0UL) >> (l)` 這段敘述中,為什麼要右移,如果只是想要在尾端補 0 的時候,那只要左移 `l` 次就可以了 也就是在右半部分只要 `(~0UL) << (l)` 能達到一樣的效果 ::: ## 測驗2 考慮以下程式碼 ```c struct foo; struct fd { struct foo *foo; unsigned int flags; }; enum { FOO_DEFAULT = 0, FOO_ACTION, FOO_UNLOCK, } FOO_FLAGS; static inline struct fd to_fd(unsigned long v) { return (struct fd){EXP1, v & 3}; } ``` 函式 `fo_fd` 接受指定的地址 `v`,填入一個 `fd` 結構體,並確保第一個成員的地址,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 `align_down`)。 :::info >參考[C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory?type=view) * 首先要先了解電腦的 CPU 如何抓取資料,CPU 抓取資料的單位是 4 byte 或是 8 byte (分別為 32 位元與 64 位元的電腦架構),在處理上才會快速,不會想一個一個 1 byte 抓取 ![](https://i.imgur.com/ln9PLF3.png) * 若資料的分布狀況不在 4 byte 的情況下,如下圖,分別抓取藍色的資料與黃色的資料,在透過移位(shift)的方式調整所需要的資料,最後再結合一起 * 但是這樣的做法比較沒有效率,所以編譯器在分配記憶體時就會做 data alignment 的動作 ![](https://i.imgur.com/RUuJ9da.png) * 舉例在 int 的 alignment 方式,int 為 4 byte 、 char 為 1 byte ,理論上加起來要為 5 byte , 但是 char 為了 alignment int,所以會從 1 byte 多加 3 個 byte,變成 8 byte 的形式,所以 CPU 就能因為這樣的倍數關係不會降低抓取資料的速度 ```c struct s1 { char c; int a; } ``` ::: * 對 4 個位元組向下對齊,因此要將 `v` 的值處理能被 4 整除的數字,所以在二進位元的表示中,在最後兩位的 bit 要等於 0 * 所以 3 的二進位元為 00..0011~2~ , 在透過取反獲得 11..1100~2~ ,再透過 `&` 運算子可將 v 的最後兩個 bits 變成 00~2~ * 回傳的結構是 `struct fd` 所以在將型態強制轉型成結構內的型態 `struct foo *` ,始能將 `v` 記憶體位址取址 :::success EXP1 = (struct foo *)(v & ~3) ::: 測試: ```c long long a = 5; struct fd test = to_fd(a); printf("alignment : %p %p\n", &test.foo, &test.flags); ``` 結果: ``` alignment : 0x7fff11a83dd0 0x7fff11a83dd8 ``` 可以看到在記憶體的表現上以 8 byte 的空間分配 ## 測驗3 考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 [LeetCode 190. Reverse Bits](https://leetcode.com/problems/reverse-bits/) 的描述。 ```c #include <stdint.h> uint8_t rev8(uint8_t x) { x = (x >> 4) | (x << 4); x = ((x & 0xCC) >> 2) | (EXP2); x = ((x & 0xAA) >> 1) | (EXP3); return x; } ``` * 觀察函式中 `0xCC` 與 `0xAA` 在 8 位元下的二進位表示分別為 `11001100` 與 `10101010`,利用 `&` 的運算子,可將對應位置的 bit 紀錄,也就是指定相對應的 bit 進行 shift * 可得知在交換順序時候為 * 前 4 個與後 4 個 bit 交換 * 再來鄰近每兩個 bit 成對交換 * 最後將鄰近一組的 2 個 bit 互換 * 所以在左半部分都是對 x 數值向右 shift,可推測右半部分是對 x 數值向左 shift * 因此將 `0xCC` 與 `0xAA` 的二進位取反: ``` 0xCC : 11001100 ~0xCC : 00110011 /* 0x33 */ 0xAA : 10101010 ~0xAA : 01010101 /* 0x55 */ ``` :::success EXP2 = (x & 0x33) << 2 EXP2 = (x & 0x55) << 1 ::: * 以 `x = 0x35` 為例,`8-bit` 二進位為 00110101~2~,預期結果為 10101100~2~ 1. `x = (x >> 4) | (x << 4);` ``` 00000011 (x >> 4) | 01010000 (x << 4) ........... 01010011 (4 個 4 個交換) ``` 2. `x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);` ``` 01010011 (x) 01010011 (x) & 11001100 (0xCC) & 00110011 (0x33) ........... ........... 01000000 00010011 00001000 ((x & 0xCC) >> 2) | 01001100 ((x & 0x33) << 2) ........... 01011100 (2 個 2 個交換) ``` 3. `x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);` ``` 01011100 (x) 01011100 (x) & 10101010 (0xAA) & 01010101 (0x55) ........... ........... 00001000 01010100 00000100 ((x & 0xAA) >> 1) | 10101000 ((x & 0x55) << 1) ........... 10101100 (相鄰交換) ``` ## 測驗4 延伸〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉,我們嘗試將 [foreach](https://en.wikipedia.org/wiki/Foreach_loop) 巨集 予以擴充,使得支援以下寫法: ```c int i; foreach_int(i, 0, -1, 100, 0, -99) { printf("i is %i\n", i); } const char *p; foreach_ptr(p, "Hello", "world") { printf("p is %s\n", p); } ``` 預期輸出如下: ``` i is 0 i is -1 i is 100 i is 0 i is -99 p is Hello p is world ``` 對應的巨集定義: ```c #include <assert.h> #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) #define foreach_int(i, ...) \ for (unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0); \ _foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int); \ (i) = ((int[]){__VA_ARGS__, 0})[EXP4]) #define foreach_ptr(i, ...) \ for (unsigned _foreach_i = \ (((i) = (void *) ((typeof(i)[]){__VA_ARGS__})[0]), 0); \ (i); (i) = (void *) ((typeof(i)[]){__VA_ARGS__, \ NULL})[EXP5], \ _foreach_no_nullval(_foreach_i, i, \ ((const void *[]){__VA_ARGS__}))) ``` * 可發現巨集 `__VA_ARGS__` 主要用於宣告任意的變數在 function 的引數中,方便在宣告時不使用太多不同型態的宣告(可變引數的宣告方式),在 C99 以後的版本新增的功能 > A macro can be declared to accept a variable number of arguments much as a function can. The syntax for defining the macro is similar to that of a function. > 來源 : [gcc 文件](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) * 程式的功能在於要能走訪 `foreach_int` 函式中的每一個 `int` 的引數,觀察到函式定義中的 `i` 即代表每一個引數的變數,所以變數 `i` 為範例中的 `(0, -1, 100, 0, -99)` * 分析 for 迴圈的初始條件 `unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0);` * 拆部分來看 `((i) =((int[]){__VA_ARGS__})[0])` ,前面的 `int[]` 表示整數的陣列宣告,放入的資料為 `__VA_ARGS__` 也就是說將函式的引數值都存入了陣列當中 `(0, -1, 100, 0, -99)` ,而 `[0]` 表示陣列第一個位置的元素,以範例來說是 `0` * 再來是給定 `_foreach_i` 變數值為括弧後的 `0` ,就好像是宣告了某個變數的過程時,再宣告一個變數,`_foreach_i` 也不會拿到 `i` 的值 * 看到判斷條件 `_foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int)` 右半部分的內容,計算整個陣列的記憶體長度,除上 `int` 的長度,也就是計算整個陣列元素的數量,所以就可以觀察到 `_foreach_i` 的變數用於走訪的次數,會根據傳入引數值的個數 * `(i) = ((int[]){__VA_ARGS__, 0})[EXP4]` 所以根據上述的分析,可以得知 `[EXP4]` 為整數陣列中元素的位置,需要走訪每一個元素,為持續遞增的變數,因此 `EXP4` 為 `++_foreach_i` ,所以觀察 `foreach_ptr` 函式功能也相似,所以 `EXP5` 也為 `++_foreach_i` :::success EXP4 = ++_foreach_i; EXP5 = ++_foreach_i; 一開始想法比較簡單,會選擇 `_foreach_i++`,但是經時跑過程式後發現會是錯誤的,原因是在下一次的迴圈時,變數 `i` 比 `_foreach_i++` 早賦值,因此再第一次迴圈的時候會重複 `(i) = ((int[]){__VA_ARGS__, 0})[0]` 的結果,將第一個引數重複走訪兩次 ::: * 測試 `_foreach_i++` 結果: ``` i is 0 i is 0 i is -1 i is 100 i is 0 ``` * 利用 `typeof()` 取得引數的變數型態並宣告一個陣列存取 :::info [參考 gcc 文件解釋](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) >Another way to refer to the type of an expression is with typeof. The syntax of using of this keyword looks like sizeof, but the construct acts semantically like a type name defined with typedef. ::: * 對於 `const char *` 處理方式透過 `void *` 可以彈性處理傳入的型態,可用於指向不同型態的變數,有暫存的效果,可以再還原 [void * 參考文章](https://medium.com/@racktar7743/c%E8%AA%9E%E8%A8%80-%E6%8C%87%E6%A8%99%E6%95%99%E5%AD%B8-%E4%BA%94-1-void-pointer-c1cb976712a3) * 範例: ```c int a = 10; void *void_ptr = &a; printf("%d\n", *(int *)void_ptr); /* 結果為 10 */ ``` * 使用 assert() 函式,用來檢驗敘述的正確性,在本範例中要檢驗每個資料的是否存在 [assert() 參考文章](https://www.itread01.com/content/1550463517.html) ```c #define _foreach_no_nullval(i, p, arr) \ assert((i) >= sizeof(arr) / sizeof(arr[0]) || (p)) ``` * 若 `p` (元素)為 `NULL` 且 `i` 的數量少於陣列元素的個數,所以為了確保函式能順利走訪每一個資料 * 當 `assert` 情況產生,出現以下的訊息: ```shell main: Assertion `(_foreach_i) >= sizeof(((const void *[]){"Hello", "world"})) / sizeof(((const void *[]){"Hello", "world"})[0])' failed. ``` ## 測驗5 針對 [LeetCode 29. Divide Two Integers](https://leetcode.com/problems/divide-two-integers/),以下是可能的實作: ```c= #include <limits.h> int divide(int dividend, int divisor) { int signal = 1; unsigned int dvd = dividend; if (dividend < 0) { signal *= -1; dvd = ~dvd + 1; } unsigned int dvs = divisor; if (divisor < 0) { signal *= -1; dvs = ~dvs + 1; } int shift = 0; while (dvd > (EXP6)) shift++; unsigned int res = 0; while (dvd >= dvs) { while (dvd < (dvs << shift)) shift--; res |= (unsigned int) 1 << shift; EXP7; } if (signal == 1 && res >= INT_MAX) return INT_MAX; return res * signal; } ``` * 在兩整數二進位的除法中也可以利用長除法的觀念下去執行,如圖 ![](https://i.imgur.com/t8rtJzg.png) * 先看到 4 ~ 15 行中分別處理被除數與除數的正負號問題,在執行上將兩個未知的整數都先取正數,使用的方式是利用二補數的行為取正,先取反 `~` 再 +1 * 再來看到 17 ~ 19 行中根據長除法的方式,除數要先從被除數最前端的位元開始進行,所以利用迴圈的方式檢查被除數與除數相差的位元位數 >應該不用使用到迴圈進行位元數的判斷,可以使用 `ctz` 類的相關函式進行修改 :::danger 另外,若除數為零的情況下,在 17 ~ 19 行的敘述中會進入無窮迴圈,為本題的缺陷之處 ::: :::success EXP6 = dvs << shift ::: * 看到 22 ~ 27 行,首先看到第一層的迴圈,用於判斷被除數在大於除數時能持續進行,所以 `EXP7` 會與這個判斷式有關,對於被除數或是除數的增減有影響 * 再來看到第二層的迴圈,在每一輪的除法中判斷是否可以進行相減的情況(換言之就是商數可以填入 1 的時候),若不行則退一位( `shift--` ),確保每一輪的減法是大減小的情況 * 利用 `res` 儲存商的結果,觀察到 `(unsigned int) 1 << shift` 就是由第二層迴圈判斷的結果,將商數儲存,利用 `|` 運算子根據 `shift` 移位的結果將該輪可進行一次減法的 `1` 存下 * 也就是說在 `EXP7` 中為每一輪除法的減法敘述,因此 `EXP7` 為 :::success EXP7 = dvd -= dvs << shift ::: * 最後看到 28 ~ 29 行的敘述,因為使用 `unsigned int` 儲存結果,因此要避免 overflow 的問題產生,所以把特例的情況將結果固定為 `int` 的最大數值結果 >例外的情況為當除數為 -1 或 1 的情況就會有這種狀況的發生,能讓 `res` 的最左邊第一個位置(對於有號整數來說是正負號的判斷位元)的位元為除數 -1 或 1 ## 測驗7 考慮 `ilog32` 函式可針對給定的 32 位元無號數,計算扣除開頭的 `0`,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作: ```c int ilog32(uint32_t v) { int ret = v > 0; int m = (v > 0xFFFFU) << 4; v >>= m; ret |= m; m = (v > 0xFFU) << 3; v >>= m; ret |= m; m = (v > 0xFU) << 2; v >>= m; ret |= m; m = EXP10; v >>= m; ret |= m; EXP11; return ret; } ``` * 題目解析:計算一個無號整數的二進位元表示中,最高的 1 bit 位元前扣除掉不必要的 0 位元,所需要能表現該數的有效位元數量 * 分為 4 個階段判斷: * 判斷數字是否為 16 位元以上的數值 (0xFFFFU = 111..11~2~),大於則將判別的結果 1 儲存 16 的結果,也就是 `1 << 4` = 10000~2~ = 16~10~ * 將 v 檢查完後 16 位元後就可以移除 `v >>= m` ,進行下一階段 8 個位元的檢查 * 以此類推在這 4 個階段的檢查,每一次都是對半檢查 ``` 前 3 個階段分別的 bitmask為 0xFFFF = 1111111111111111 0xFF = 11111111 0xF = 1111 ``` * 所以 `EXP10` 為 `11` 的 bitmask ,轉為 16 進制 = 0x3U :::success EXP10 = (v > 0x3U) << 1 ::: * 剩下還有最後一位 bit 的檢查,用要將 ret 結果的最後一個 bit 確認 1 的與否,與前面的檢查方式類似,只不過要濃縮成一句,要達成兩件事情: 1. 檢查最後 1 的 bitmask 2. 將結果填入 1 或 0 * 根據前面的檢查方式可以變成以下兩句: 1. m = (v > 0x1U) << 0; 2. ret += m; >流程上需要 v >> m; 用於下一階段的檢查,但已是最後一次的判斷所以不用再移位 * 將兩句結合,0x1U = 1~2~ ,`<< 0` 沒有實質作用所以刪除,再將 ret 結果補上 1 或 0 剛好為判斷式的結果,因此 :::success EXP11 = ret += v > 1 ::: ## 測驗9 考慮以下進行記憶體地址對齊 ([alignment](https://en.wikipedia.org/wiki/Data_structure_alignment)) 的巨集: ```c /* maximum alignment needed for any type on this platform, rounded up to a power of two */ #define MAX_ALIGNMENT 16 /* Given a size, round up to the next multiple of sizeof(void *) */ #define ROUND_UP_TO_ALIGNMENT_SIZE(x) \ (((x) + MAX_ALIGNMENT - MMM) & ~(NNN)) ``` * 題目解析:巨集功能要能把傳入引數進行 `alignment` 的動作,將 1 ~ 16 的數值,都改為 16~10~ 也就是 10000~2~最後的 4 個 bit 要為 0,若傳入 0 則要回傳 0 * 而後 4 個位元的的數值為 1~10~ ~ 15~10~ (0001~2~ ~ 1111~2~),但這不是我們要的,因此要把它清零,所以要產生 0000 的 0 bitmask,所以可以推測 `~(NNN)` 為相關作法 * `NNN` 產生 0000~2~ 的方法為對 1111~2~(15~10~) 取反,如何找出 15 這個數字,就是由 16 - 1 得來 > 如果要產生 000 的 bitmask 也可以利用 8~10~ -> 1000~2~,再 - 1 為 0111,最後取反得到 1000 :::success NNN = MAX_ALIGNMENT - 1 ::: * 獲得 0 的 bitmask 後就可以用 `&` 計算子清零,理解後就可以觀察到前面的敘述: `((x) + MAX_ALIGNMENT - MMM)` , `x + MAX_ALIGNMENT` 將 x 加上不影響的最後 4 個位元的數值(保留第 5 位元 16 的值)再搭配清零的動作就可以得到 16 的數值 * 那假設傳入的是 0 代表要把加入的 16 清除,才能回傳 0,所以 MMM 就是在處理 0 的特例情況,要把 10000~2~ 降一位透過 -1 的方式進行(可得到 01111~2~) :::success MMM = 1 ::: * x 傳入 1 ~ 15 範圍的值 ``` 以 8 bits 表現 x 00000110 MAX_ALIGNMENT 00010000 x + MAX_ALIGNMENT 00010110 ^ (保留 alignment 值) MAX_ALIGNMENT - 1 00001111 取反 `~` -> 11110000 00010011 x + MAX_ALIGNMENT - 1 (-1不影響結果) & 11110000 ~(MAX_ALIGNMENT - 1) ........... 00010000 <- 16 的結果 ``` * x 傳入 0 ``` 以 8 bits 表現 x 00000000 MAX_ALIGNMENT 00010000 x + MAX_ALIGNMENT 00010000 MAX_ALIGNMENT - 1 00001111 取反 `~` -> 11110000 00001111 x + MAX_ALIGNMENT - 1 (-1 將最高位降一位) & 11110000 ~(MAX_ALIGNMENT - 1) ........... 00000000 <- 0 的結果 ``` ## 測驗10 考慮以下巨集可得到二個表示式進行整數除法操作時,最接近的整數值: ```c #define DIVIDE_ROUND_CLOSEST(x, divisor) \ ({ \ typeof(x) __x = x; \ typeof(divisor) __d = divisor; \ (((typeof(x)) -1) > 0 || ((typeof(divisor)) -1) > 0 || \ (((__x) > 0) == ((__d) > 0))) \ ? ((RRR) / (__d)) \ : ((SSS) / (__d)); \ }) ``` 注意: 當除數 (即 `divisor`) 為負值時,行為沒有定義。 參考執行結果: * `DIVIDE_ROUND_CLOSEST(7, 3) = 2` * `DIVIDE_ROUND_CLOSEST(6, 3) = 2` * `DIVIDE_ROUND_CLOSEST(5, 3) = 2` 看到前三個敘述的判斷條件,分別為: * `((typeof(x)) -1) > 0` * `((typeof(divisor)) -1) > 0` * `(((__x) > 0) == ((__d) > 0))` 前兩項為:將傳入 `x` 、 `divisor` 的型態轉型到 `-1` 上,分兩種情況為有號數與無號數,這個判斷式就是要確保在計算的時候皆為無號數,所以透過 `-1` 的強制轉型判斷 ```c unsigned int a; int b; printf("result : %d\n", ((typeof(a)) -1) > 0); printf("result : %d\n", ((typeof(b)) -1) > 0); ``` 結果為: ``` result : 1 result : 0 ``` 而第三項的判斷式,也是確保傳入的兩數皆為正數的情況下進行計算 * 再來看到除法的取最接近整數的運算方式, `/` 在運行的方式為無條件捨去小數的形式,所以要透過對結果有四捨五入的結果產生,所以要對 `/` 結果 + - 除數的一半數值,也就是說要將***被除數捨棄掉的數值進行處理*** * 若是捨棄掉除數的一半以上則需要加上除數的 0.5 進位 * 若是捨棄掉除數的一半以下則需要扣除除數的 0.5 :::success RRR = (__x) + ((__d) >> 1) SSS = (__x) - ((__d) >> 1) ::: ## 測驗11 考慮以下計算整數開平方根的實作: ```c static inline unsigned long fls(unsigned long word) { int num = 64 - 1; if (!(word & (~0ul << 32))) { num -= 32; word <<= 32; } if (!(word & (~0ul << (64 - 16)))) { num -= 16; word <<= 16; } if (!(word & (~0ul << (64 - 8)))) { num -= 8; word <<= 8; } if (!(word & (~0ul << (64 - 4)))) { num -= 4; word <<= 4; } if (!(word & (~0ul << (64 - 2)))) { num -= 2; word <<= 2; } if (!(word & (~0ul << (64 - 1)))) num -= 1; return num; } unsigned long i_sqrt(unsigned long x) { unsigned long b, m, y = 0; if (x <= 1) return x; m = 1UL << (fls(x) & ~1UL); while (m) { b = y + m; XXX; if (x >= b) { YYY; y += m; } ZZZ; } return y; } ``` * `i_sqrt` 函式的作用等同於 `floor(sqrt(x))` 首先看到 `fls()` (find the last bit set)函式的作用,目的是要找到一數在二進位中由最低位至到最高位的 `1` 的位置,檢查的方式為產生 1 的 bitmask 在不同位置檢查,檢查的順序為 32 位元、 48 位元、 56 位元、 60 位元、 62 位元、 63 位元,遞增的順序為 +16 +8 +4 +2 +1,以 4~10~ (100~2~) 為例 ``` 第一層: num = 63 1111...1100...000 (~0ul << 32) 前 32 個位元為 1 & 100 .................... 00000000000000000 num = 63 - 32 = 31 第二層: (看前 32 個位元) num = 31 1111...11000...000 (~0ul << 48) & 100 ( 4 << 32) .................... 0000...0000...000 num = 31 - 16 = 15 第三層: (看前 16 個位元) num = 15 1111111100000000 (~0ul << 56) & 100 ( 4 << 16) .................. 0000000000000000 num = 15 - 8 = 7 第四層: (看前 16 個位元) num = 7 1111000000000000 (~0ul << 60) & 10000000000 ( 4 << 8) .................. 0000000000000000 num = 7 - 4 = 3 第五層: (看前 16 個位元) num = 3 1100000000000000 (~0ul << 62) & 100000000000000 ( 4 << 4) .................. 0100000000000000 結果不為 0, num = 3 第六層: (看前 16 個位元) num = 3 1000000000000000 (~0ul << 62) & 100000000000000 .................. 0000000000000000 num = 3 - 1 = 2 4 = 0100 最高位 1 為第 2 個位元位置 (第一個位置是第 0 位) ``` * `m` 透過 `fls()` ,找出數字的最高位元位置,目的為了找出小於該數值最大二進位中完全平方數,(例如: 1, 4, 16, 64),用 `& ~1UL` 將 `fls()` 的最低位數去除 * 參考 [平方根的算法1](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2))、[平方根的算法2](https://hackmd.io/@sysprog/sqrt?type=view) 首先一個數可假設$$N^2 = (a_n+...a_0)^2$$ 而在二進位中可將每一項的 $a$ 表示為 $a_m = 2^m$ 或是 $a_n = 0$ 可由二進位的數值進行逼近,對 $2^m$ 進行迭代,假設為:$$P_m = a_n + a_{n-1}+...+a_m$$ >m+1為迭代的下一位 決定$a_m = 2^m$ 或是 $a_n = 0$,假設$$P_m=P_{m+1} + 2^m \ \ -(1)$$ 若 $N^2 \geq P_m^2$,則表示 $a_m = 2^m$ (表示$a_m$ 存在於平方數中),若無則 $a_m = 0$ 根據上式則為 $P_m=P_{m+1}$,而為防止迭代停止的狀況,則為更新目標數字的數值為$$X_m = N^2 - P_m^2$$,持續更新數值為:$$X_m = X_{m+1} - Y_m$$,其中 $$Y_m = P_m^2 - P_{m+1}^2 = 2P_{m+1}a_m+a_m^2$$ >(由式 1 平方結果而來) 分別表示:$$c_m = 2P_{m+1}a_m, \ \ d_m = a_m^2$$ 又因$a_m = 2^m$$$c_m = P_{m+1}2^{m+1}, \ \ d_m = (2^m)^2$$ 可得在 `m-1` 時:$$c_{m-1} = P_m2^m=(P_{m+1}+a_m)2^m=P_{m+1}2^m+a_m2^m$$ 所以當 $a_m=2^m, c_m/2 + d_m$ ,而 $a_m = 0, c_m/2$,$d_{m-1} =d_m/4$ 根據上述的計算方式就可以發現對應題目中的變數 * $c_m = y$ * $d_m = m$ 對應程式為: * 每次的迭代都需要 $c_m/2$ * $d_{m-1} =d_m/4$ :::success XXX = y >>= 1 ZZZ = m >>= 2 ::: 所以當平方數在 `x` 存在則需要扣除,因此: :::success YYY = x -= b :::