# 2024q1 Homework4 (quiz3+4) contributed by < `yinghuaxia` > ## [第三週測驗](https://hackmd.io/@sysprog/linux2024-quiz3) ### 測驗一 測驗一當中使用以下的程式碼計算平方根,以下解析對其演算法: ```c #include <math.h> int i_sqrt(int N) { int msb = (int) log2(N); int a = 1 << msb; int result = 0; while (a != 0) { if ((result + a) * (result + a) <= N) result += a; a >>= 1; } return result; } ``` 首先我們先對傳入的數字 $N$ 做 `(int) log2(N)` 的運算,意思是找到 $N$ 這個數字轉成二進位後的 most signficant bit( `msb` ),原理是因為 $x = \log_2(N)$ 意即 $2^x = N$,因此對 $x$ 取整數就可以得知 $N$ 換成二進位的最高 bit 位置。接著我們將讓 `int a = 1 << msb`,讓 `a` 為 2 的 `msb` 次方,也就是讓 `a` 在小於或等於 $N$ 的情況下為 2 的最高次方。 接著先初始化 `int result = 0`,並且進入一個終止條件為 `a == 0` 的 while 迴圈。While 迴圈裡面的判斷是為:如果 `(result + a) ^ 2 <= N`,意思是 `result + a` 比 `N` 開根號的數值還要小,所以將 `result += a` 使得 `result` 逐漸逼近 $N$ 的開根號;否則代表目前 `a` 的平方仍大於 `N`,因此不對 `result` 做任何動作,然後讓 `a` 變成原本的 $\frac{1}{2}$ 再繼續進行判斷及運算。 可以改寫 `i_sqrt` 使其不用依賴 $\log_2$ 的運算: ```c int msb = 0; int n = N; while (n > 1) { n >>= 1; msb++; } ``` 這邊的寫法也就是當 $N$ 大於 1 時,讓他不停的向右移動一個 bit,計算總共移動了幾個 bit 就可以知道 $N$ 的 most significant bit 是在哪一位。 ### 測驗二 以下的程式進行 mod 10 和 div 10 的操作: ```c static void str_add(char *b, char *a, char *res, size_t size) { int carry = 0; for (int i = 0; i < size; i++) { int tmp = (b[i] - '0') + (a[i] - '0') + carry; carry = tmp / 10; tmp = tmp % 10; res[i] = tmp + '0'; } } ``` 這個函式輸入四個參數: `b`:指向第一個字串的指標,代表的是一個數字 `a`:指向第二個字串的指標,代表的是一個數字 `res`:指向一個字串的指標,代表的是結果會被儲存的位置 `size`:字串的大小 `carry` 這個變數被初始化為 0,用來儲存在加法時的進位 `-'0'` 的目的是為了將字串轉換為數字的形式,並對兩個字串進行從個位數開始的加法運算。將兩個數值該位數的相加暫存到 `tmp` 中,如果在該位數兩個數值相加有進位的情形,會因為 `tmp / 10` 操作而把進位的數值存到 `carry` 這個變數中;接著對 `tmp % 10` 得到該位數的數值。透過 for 迴圈對每一個位數做相同的操作,就可以得到兩個數值相加的結果。 ### 測驗三 以下程式是進行 $\log_2$ 運算的方式,對其內容作解析: ```c int ilog2(int i) { int log = -1; while (i) { i >>= 1; log++; } return log; } ``` 先將輸入的 `i` 轉換為二進位,因為 $log = \log_2(i)$ 意即 $2^{log} = i$,我們想要求的是 $i$ 可以被轉換為 2 的冪,因此透過 while loop 來對 $i$ 做逐個 bit 的右移,用 `log` 來紀錄 `i` 的二進位總共有幾個 bit 即可得到取 $\log_2$ 的結果。 改寫成以下: ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= AAAA) { //AAAA = 65536 result += 16; i >>= 16; } while (i >= BBBB) { //BBBB = 256 result += 8; i >>= 8; } while (i >= CCCC) { //CCCC = 16 result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` 這個方式可以更快速的知道輸入的 `i` 換成二進位之後有幾個 bit。一開始在 while loop 裡面會判斷 `i` 是否大於 $2^{16}$,如果這個條件為真,則代表 `i` 的 msb 後至少有 16 個 0,因此可以直接把 `result` 加上 16,下面的判斷是也是同樣的道理,希望可以一次移動較多 bit 來減少逐個移動的時間成本。因此 `BBBB` 我們填入 $2^{8} = 256$;`CCCC` 填入 $2^4 = 16$;`DDDD` 填入 $2^1$。 我們也可以使用 gcc 內建函式 `__builtin_clz` 來達到一樣的結果。該內建函式在 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fclz) 的解釋是: > 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. 也就是這個函式會回傳從最高位 (msb) 之前的連續的 0 的個數,如果輸入 0 則行為不會被定義。可以使用下面的程式碼對輸入值進行取 $\log_2$: ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(DDDD)); //DDDD = v | 1 } ``` `31 - (__builtin_clz(v | 1))`:這個操作會將 `v` 的 least significant bit 設為 1,確保可以得到至少有一個 bit 不是 0 的數值,再透過 `__builtin_clz` 函式找到 msb 前面有幾個連續的 0,用 31 位元減掉前面連續的 0 的部分就可以知道 `v` 的最高位元的位置。因為 `v` 最多只有連續的 31 個零 (因為 `v | 1` 的操作),因此使用 31 對 `__builtin_clz` 得到的結果做相減。 在 Linux 核心提供 [include/linux/log2.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/log2.h) 標頭檔,裡面包含一些基於 $\log_2$ 的運算,像是 `__ilog2_u32`、`__ilog2_u64` 等,主要會使用在處理記憶體管理和資料結構等相關的任務時會使用到。舉例來說: * `linux / arch`: 跟架構相關的檔案裏面會使用 `log2`,來做記憶體相關的管理。 * `drivers/`:在處理裝置初始化、配置相關或是處理資料時會使用到 `log2`,如 [drivers/dma/xilinx/zynqmp_dma.c](https://github.com/torvalds/linux/blob/317c7bc0ef035d8ebfc3e55c5dde0566fd5fb171/drivers/dma/xilinx/zynqmp_dma.c#L536)所示,該函式主要是在配置 `ZynqMP DMA channel`。 ```c static void zynqmp_dma_config(struct zynqmp_dma_chan *chan) { u32 val, burst_val; val = readl(chan->regs + ZYNQMP_DMA_CTRL0); val |= ZYNQMP_DMA_POINT_TYPE_SG; writel(val, chan->regs + ZYNQMP_DMA_CTRL0); val = readl(chan->regs + ZYNQMP_DMA_DATA_ATTR); burst_val = __ilog2_u32(chan->src_burst_len); val = (val & ~ZYNQMP_DMA_ARLEN) | ((burst_val << ZYNQMP_DMA_ARLEN_OFST) & ZYNQMP_DMA_ARLEN); burst_val = __ilog2_u32(chan->dst_burst_len); val = (val & ~ZYNQMP_DMA_AWLEN) | ((burst_val << ZYNQMP_DMA_AWLEN_OFST) & ZYNQMP_DMA_AWLEN); writel(val, chan->regs + ZYNQMP_DMA_DATA_ATTR); } ``` 其中 `burst_val = __ilog2_u32(chan->src_burst_len)` 透過 `ilog2_u32` 得到 register 設定 burst length 的 offset。 * linux / fs / ocfs2:是一個共用磁碟檔案系統,主要用於 Oracle Real Application Clusters (RAC) 環境。他提供 Linux 系統在運行 Oracle 資料時可以有共用儲存存取的功能,提升系統的可用性和可擴充性。在 OCFS2 中,ilog2 用於計算區塊大小、確定資料結構索引或最佳化儲存分配演算法等任務。 ### 測驗四 [Exponentially Weighted Moving Average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) 是一種取平均的方式。其原理是將較早的輸出搭配較低的權重,意思是較早的輸出對現在的輸出影響力較低;反之,較晚的輸出則對現在輸出的影響力較高。而權重與輸出的搭配與 $(1 - \alpha) ^ t$ 有關。$\alpha:$ 權重隨時間過去降低的程度; $t:$ 時間。以下對 EWMA 的實作做說明: ```c #include <assert.h> /* Exponentially weighted moving average (EWMA) */ struct ewma { unsigned long internal; unsigned long factor; unsigned long weight; }; ``` 定義一個 `ewma` 的架構,包含三個 `unsigned long` 型態的變數,分別是 `internal`、`factor` 和 `weight`。 * `internal` 是用來儲存 moving average 現在的狀態,會隨著新的輸出被加到 moving average 中而更新。 * `factor` 表示的是 `internal` 的縮放倍率,影響 `internal` 的精度和粒徑。 * `weight` 代表新舊輸出的權重,`weight` 愈高表示新的輸出對整體的平均有較大的影響;反之則表示可以更長時間的保留舊的輸出所造成的影響。通常會被設定為 2 的冪使得計算有效率。 ```c /* This section contains universal functions designed for computing the EWMA. * It maintains a structure housing the EWMA parameters alongside a scaled * internal representation of the average value, aimed at minimizing rounding * errors. Initialization of the scaling factor and the exponential weight (also * known as the decay rate) is achieved through the ewma_init(). Direct access * to the structure is discouraged; instead, interaction should occur * exclusively via the designated helper functions. */ /** * ewma_init() - Initialize EWMA parameters * @avg: Average structure * @factor: Scaling factor for the internal representation of the value. The * highest achievable average is determined by the formula * ULONG_MAX / (factor * weight). To optimize performance, the scaling * factor must be a power of 2. * @weight: Exponential weight, also known as the decay rate, dictates the rate * at which older values' influence diminishes. For efficiency, the weight * must be set to a power of 2. * * Initialize the EWMA parameters for a given struct ewma @avg. */ void ewma_init(struct ewma *avg, unsigned long factor, unsigned long weight) { if (!is_power_of_2(weight) || !is_power_of_2(factor)) assert(0 && "weight and factor have to be a power of two!"); avg->weight = ilog2(weight); avg->factor = ilog2(factor); avg->internal = 0; } ``` `ewma_init` 對 EWMA 的參數進行初始化。在 `if` 判斷式中,如果 `weight` 和 `factor` 不是 2 的冪,則 assert 一個錯誤訊息,以確保這兩個參數的設置需要符合計算的效率。接著將 `avg` 的 `weight` 和 `factor` 分別設為 `weight` 和 `factor` 的對數,並將 `avg` 的 `internal` 設為 0,開始 EWMA 的計算。 ```c /** * ewma_add() - Exponentially weighted moving average (EWMA) * @avg: Average structure * @val: Current value * * Add a sample to the average. */ struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal ? (((avg->internal << EEEE) - avg->internal) + // EEEE = avg -> weight (val << FFFF)) >> avg->weight // FFFF = avg -> factor : (val << avg->factor); return avg; } ``` `ewma_add()` 函式實作了簡單的 EWMA 流程。判斷式會檢查 `avg -> internal` 是否為 0,若不為 0 則表示現在已經有輸出的數值存在 `avg -> internal` 中,那運算就需要遵從 EWMA 的公式,也就是將已經有的舊輸出乘上 $1 - weight$,因為我們的 `weight` 已經取過對數,因此我們可以使用平移的方式做乘法的運算,在程式碼中寫成 `((avg->internal << avg -> weight) - avg->internal)` 的形式,再加上新的輸出經過 `factor` 的調整後乘上權重;若為 0 則表示沒有已經存在的輸出數值,只需要將現在的輸出數值 `val` 根據 `avg -> factor` 做縮放並存在 `avg -> internal` 中。 ```c /** * ewma_read() - Get average value * @avg: Average structure * * Returns the average value held in @avg. */ unsigned long ewma_read(const struct ewma *avg) { return avg->internal >> avg->factor; } ``` 此函數回傳儲存在 ewma 結構 `avg` 中的平均值,並按 `factor` 右移。透過右移 `avg->factor`,函數會撤銷在 EWMA 計算期間實施在平均值的縮放。 在 linux kernel 中,EWMA 可以用來平滑和預測資源的使用方式,以利於資源的分配與排程。舉例來說,CPU 資源的調度可以藉由 EWMA 的方式做動態的工作負載調整,像是在 [linux/kernel/sched/fair.c](https://github.com/torvalds/linux/blob/master/kernel/sched/fair.c) 的 `util_est_update` 函式中,EWMA 的功能在於計算執行一個任務的虛擬運算時間,透過知道一個任務的虛擬運算時間,可以反應 CPU 的使用情況,進而影響一個任務的優先順率。`fair.c` 這個檔案本身實現了 Linux kernel 中的 CFS 調度器,旨在確保 CPU 資源的公平分配。 ### 測驗五 延伸測驗三,計算 [ceil](https://man7.org/linux/man-pages/man3/ceil.3.html) 和 [log2](https://man7.org/linux/man-pages/man3/log2.3.html) 的組合並轉型成整數: ```c int ceil_ilog2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x > 1) + 1; } ``` 先將輸入值減 1(`x--`),這麼做的目的是確保如果 x 不是 2 的冪時,計算出的對數會向上取整。因為 `x--` 會將 x 減少 1,所以如果 x 原本是 2 的冪,減 1 後會導致其二進位表示最高位外的其他位全部為 1,而 x 原本不是 2 的冪時,減 1 會將最高位以外的第一個 1 轉換為 0。透過右移 x 的來檢查 x 的最高有效位元。每次,它都會檢查 x 是否大於 2 的某些冪(0xFFFF、0xFF、0xF 和 0x3),如果是,則設定 shift 對應的位元數。在每次右移操作後,將右移的位數添加到一個變數 r 中,這個變數儲存了最高非零位所在的位置。 最後回傳的值是 `(r | shift | x > 1) + 1`,`r` 和 `shift` 變數保存了找到的最高位的位置,而 `x > 1` 是判斷變數 `x` 是否大於 1,判斷為真則將最低位設置為 1;判斷為假則最低位為 0,需做此判斷的原因在於判斷 `x > 0x3` 時,在 `x == 0x2` 或 `x == 0x3` 的情況下,則 `shift` 為 0,透過多做這個判斷可以避免這個情形的發生。`+ 1` 的目的則是將整個表達式的值加 1,這是為了確保不論 `x` 是否為 2 的冪,結果都會向上取整。 ## 第四週測驗 [題目](https://hackmd.io/@sysprog/linux2024-quiz4) ### 測驗一 popcount 是用來計算一個數值轉成二進位之後有多少位元為 `1`,以下進行實作說明: ```c unsigned popcount_naive(unsigned v) { unsigned n = 0; while (v) v &= (v - 1), n = -(~n); return n; } ``` 一開始先定義一個無號數來記錄被設置為 `1` 的位元數量,之後進入一個 while 迴圈做 bit 數的計算,其條件為 `v` 不為 `0`,也就是 `v` 中還有位元為 `1` 時會不斷進行運算。在迴圈中使用了一個叫做 [Brian Kernighan's](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiP9Yb6irWFAxXnaPUHHUUTDkgQFnoECBwQAw&url=https%3A%2F%2Fyuminlee2.medium.com%2Fbrian-kernighans-algorithm-count-set-bits-in-a-number-18ab05edca93&usg=AOvVaw3WIaFt_UL3M2EmRi2lC0el&opi=89978449) 的演算法,其想法如下所述: > Subtract one from n to flip bits after the rightmost set bit (including the rightmost set bit itself) and use n & (n-1) expression to clear the rightmost set bit of n until n becomes zero. The number of times we clear the rightmost set bit of n will be the set bit count. 做`v - 1` 的運算時,會將 `v` 當中最右邊的 `1` 位元變成 0,並將該位元右邊的 `0` 變成 `1`,再將 `v` 和 `v - 1` 做 `&` 的操作時,會將 `v` 最右邊的 `1` 位元變成 `0`。舉例 $v = 22 = 10110_2$ 來說 (下圖以 `n` 表示): ![image](https://hackmd.io/_uploads/rJF5r2MlC.png) > 出處: [Brian Kernighan's Algorithm: Count set bits in a number](https://yuminlee2.medium.com/brian-kernighans-algorithm-count-set-bits-in-a-number-18ab05edca93) `n = -(~n)` 則是先將 `n` 取反,再對其取二補數,也就是再取反後加一,也因此這邊的操作就是在將 `n` 隨著 while 迴圈對 `v` 位元的計算加 1。 因此上面的 `popcount_naive` 執行時間會取決於輸入 `v` 的位元數量,為了將其變成常數時間複雜度的操作,我們做以下的改寫: ```c unsigned popcount_branchless(unsigned v) { unsigned n; n = (v >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; n = (n >> 1) & 0x77777777; v -= n; v = (v + (v >> 4)) & 0x0F0F0F0F; v *= 0x01010101; return v >> 24; } ``` 為了使用公式 $popcount(x) = x - \lfloor \frac{x}{2}\rfloor - \lfloor \frac{x}{4}\rfloor -...- \lfloor \frac{x}{2^{31}}\rfloor$ 來完成 popcount 的計算 (原理參見[第四週測驗題](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-1)),我們使用 `n = (v >> 1) & 0x77777777`的方式做運算。`v >> 1` 的操作其實就是在做 $\lfloor{\frac{x}{2}}\rfloor$ 的動作,對 `0x77777777` 做 `&` 運算其實是想要得到在 `v` 的每個 4 位元中計算出 1 的位元數,因為 $7 = 0111_{2}$。這樣做的好處是可用四位元 (即 [nibble](https://en.wikipedia.org/wiki/Nibble)) 為單位逐步計算出 32 位元數字中為 1 的位元數,不需要使用迴圈,進而提高運算效率。 經過三次 `v -= n` 的運算之後會得到 $v - \lfloor\frac{v}{2}\rfloor - \lfloor\frac{v}{4}\rfloor - \lfloor\frac{v}{8}\rfloor$ 最後做 `v >> 4` 的原因是,我們在前面計算的 $v - \lfloor\frac{v}{2}\rfloor - \lfloor\frac{v}{4}\rfloor - \lfloor\frac{v}{8}\rfloor$ 會算出每 4 個位元的 `1` 位元數,因此我們將所得右移 4 個位元再與原數相加,經過 `0x0F0F0F0F` 的 masking 後就可以在奇數 byte 得到兩兩 byte 的 `1` 位元數相加數量,最後乘上 `0x01010101` 就可以在第 $2^7$ 位元處得到我們想要的 popcount 結果,因此向右移動 $2^6 = 24$ 個位元就可以在 LSB 得到最終回傳值。 兩個數值的 Hamming distance 可以藉由 `popcount` 巨集來求出所得。我們先對兩個數值做 [Exclusive or](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjz0fG3ybaFAxW2aPUHHWLLC9YQFnoECBYQAQ&url=https%3A%2F%2Fzh.wikipedia.org%2Fzh-tw%2F%25E9%2580%25BB%25E8%25BE%2591%25E5%25BC%2582%25E6%2588%2596&usg=AOvVaw2Agb7Pa0tLB52mnb8XvvBR&opi=89978449) 的運算,就可以將兩者位元相異處標示為 `1`,其餘標示為 `0`,接著再使用 `__builtin_popcount` 就可以得到答案。 ```c int totalHammingDistance(int* nums, int numsSize) { int total = 0; for (int i = 0; i < numsSize;i++) for (int j = 0; j < numsSize;j++) total += __builtin_popcount(nums[i] ^ nums[j]); return total >> AAAA; //AAAA = 1 } ``` 給定的程式碼想要計算多個數值之間兩兩的 Hamming distance,但存取每個數值的兩個 for 迴圈皆是從 `0` 到 `numSize`,因此數與數之間的 Hamming distance 會被計算兩次,因此最後我們需要將 `total` 除以 2 來得到最後正確的結果,所以要將 `total` 往右移 1 個位元。 然而,如果我們直接將上面的程式碼提交到 [LeetCode 477. Total Hamming Distance](https://leetcode.com/problems/total-hamming-distance/),會在測資有 10000 筆時發生 Time Limit Exceeded 的錯誤,是因為兩個 for 迴圈總共計算 `numsSize ^ 2` 次 popcount 的計算,但其實我們只需要做 $C^{numsSize}_2 = numsSize \times (numsSize - 1) / 2$ 次運算,因此將程式碼的 for 迴圈改成以下所示,並直接回傳 `total` 數值就可以通過。 ```diff int totalHammingDistance(int* nums, int numsSize) { int total = 0; int count = 0; for (int i = 0;i < numsSize - 1;i++) - for (int j = 0; j < numsSize;j++) + for (int j = i + 1; j < numsSize;j++) total += __builtin_popcount(nums[i] ^ nums[j]); return total; } ``` > 所有 Number 中從第 0 個 bit 開始計算全部該 bit 的 Hamming Distance ,在全部累加起來就是 Total Hamming Distance 根據以上提示,我們就可以不用使用兩兩做 XOR 和 popcount 來得到所有數字之間的 Hamming distance。 > 第 0 個 bit 觀察 : 全部都是 1 -> Hamming Distance = 0 第 1 個 bit 觀察 : 1 有一個,其餘皆為 0 可以直接用單個 bit 判斷 Hamming Distance (1)* (numsize - 1) 第 2 個 bit 觀察 : 1 有兩個,其餘皆為 0 Hamming Distance (2)* (numsize - 2) 實作如下: ```c int totalHammingDistance(int* nums, int numsSize) { int ans = 0; for(int i = 0; i < 32; i++){ int ones = 0; for(int j = 0; j < numsSize; j++) ones += nums[j] >> i & 0b1; ans += ones * (numsSize - ones); } return ans; } ``` 第一層 for 迴圈存取數值中的 32 個位元,第二個 for 迴圈則是存取所有要比較的數值。透過 `ones = nums[j] >> i & 0b1` 可以知道有多少數字的 `i` 位元跟 `0b1` 做 `&` 為 `1`,也就可以透過 `ones * (numsSize - ones)` 得到該位元會造成的 Hamming distance 大小。 比較兩者執行時間,前者花了 1764 ms 而後者僅花了 19 ms,兩者的差異除了 for 迴圈執行的次數外,也需考慮巨集展開所耗費的成本。 ### 測驗二 在除數符合 $2^k \pm 1$ 的情況下,可以用下述的方式算出某數除以另一數的餘數。根據[第四週測驗題](https://hackmd.io/@sysprog/linux2024-quiz4#%E6%B8%AC%E9%A9%97-2)中提出的歸納原理,以除數為 3 為例,餘數 $n$ 應為 $\Sigma_{i = 0}^{n - 1} \ b_i \cdot (-1)^i (mod\ 3)$。此公式可以寫成 `n = popcount(n & 0x55555555) - popcount(n & AAAAAAAA)`。`n & 0x55555555` 的意思是取 `n` 的奇數位元,`n & 0xAAAAAAAA` 的意思是取 `n` 的偶數位元,對應公式中偶數位元會乘上 $-1$,奇數位元會乘上 $1$,因此有上面呈現的寫法,解釋這個表示方式為 `n` 中奇數位和偶數位中 $1$ 的數量之差。 接著我們可以透過 $popcont(x \& \bar m) - popcount(x \& m) = popcount(x \oplus m) - popcount(m)$ 將 $n$ 表示為 `popcount(n ^ 0xAAAAAAAA)`,原因是因為當我們對 $n$ 和 `0xAAAAAAAA` 做 XOR 運算時,由於 `0xAAAAAAAA` 的二進位表示法為所有偶數位都為 $1$,所有奇數位都為 $0$,因此會將 $n$ 中偶數位的位元進行反轉,奇數位不變,因此省略原公式中將奇數位清零的動作。需要減去 $16$ 的原因在於我們忽略了減法借位的運算,偶數位在 $32$ 位元的數值中有 $16$ 個,因此需要將我們進行 XOR 忽略的 $16$ 進行扣除的動作,這樣就會造成結果介於 $-16$ 到 $16$ 間,因此需要再加上一個三的倍數來保證餘數為正,文中給的例子是加上 $39$。 [《Hacker's Delight》](https://web.archive.org/web/20180517023231/http://www.hackersdelight.org/divcMore.pdf) 中有說明選 $39$ 這個數值的原因: > We want to apply this transformation again, until n is in the range 0 to 2, if possible. But it is best to avoid producing a negative value of n, because the sign bit would not be treated properly on the next round. A negative value can be avoided by adding a sufficiently large multiple of 3 to n. Bonzini’s code, shown in Figure 10–21, increases the constant by 39. This is larger than necessary to make n nonnegative, but it causes n to range from –3 to 2 (rather than –3 to 3) after the second round of reduction. This simplifies the code on the return statement, which is adding 3 if n is negative. The function executes in 11 instructions, counting two to load the large constant. 這個步驟可以防止 $n$ 在第二輪縮減之後範圍不會介於 $-3$ 到 $3$ 之間,讓回傳值不需要再執行「如果結果為負則加三」的動作。 下面舉例說明: ```c int mod3(unsigned n) { n = popcount(n ^ 0xAAAAAAAA) + 23; n = popcount(n ^ 0x2A) - 3; return n + ((n >> 31) & 3); } ``` 首先,我們將 $n$ 和 `0xAAAAAAAA` 做 XOR 的運算,取 `popcount` 之後再加 $23$,避免 `n` 為負數。`0x2A` 展開是 `0x00101010`,也因此`n ^ 0x2A` 的運算會保留奇數位元,並改變第 2、4、6 位元的數值。 :::info :bulb: 加上 $23$ 這個數值是否有特別原因?為何需要做 `popcount(n ^ 0x2A) - 3;` 及回傳 `n + ((n >> 31) & 3)`? 是否跟 tic-tac-toe 作業有關連? ::: 另外也可以透過計算 $n$ 和 `0xAAAAAAAA` 做 XOR 後做 `popcount` 得到的數值進行查表,得到我們要的結果。 針對 [tictactoe.c](https://gist.github.com/jserv/9088f6f72a35a1fcaaf1c932399a5624) 中部份被遮蔽的程式碼進行說明: 文件中的輸出是模擬一百萬次棋局的結果,棋盤的大小為 3 x 3,引此玩家有 9 種可能的位置選擇。根據 [vax-r](https://hackmd.io/@vax-r/linux2024-homework4#%E6%B8%AC%E9%A9%97%E4%BA%8C1) 的解釋: > 相較於把下棋看成在九宮格中選擇一格並標記,可以把棋局看成是 8 種可能的路線(三條橫線、三條縱線、兩條對角線,分別對應到 uint32_t 以 16 進位表示的不同位元,第一條橫線為 msb,第二條橫線為 msb - 1,依此類推。 觀察 `move_mask` 可以發現,`move_mask[0] = 0x40040040`,是針對將九宮格中第一、五、九的位置標記來贏得遊戲,也就是第一列:同理 `move_mask[1]` 和 `move_mask[2]` 分別代表的是第二列和第三列。如果對三者做 OR 運算會得到 `0x70044444`,也就是當有連成一直線的情況發生時會有一個位元為 7,因此透過將 `board + 0x11111111` 的操作,再和 `0x88888888` 做 AND 運算就能得知是否有獲勝的情況發生。 ```c /* Determine if the tic-tac-toe board is in a winning state. */ static inline uint32_t is_win(uint32_t player_board) { return (player_board + BBBB) & 0x88888888; //BBBB = 0x111111111 } ``` 由於 `x & UINT32_C(0x7FFF)` 的操作只會保留最低的 15 位,因此在 `CCCC` 填入 $15$ 來減小 `x` 的數值避免 overflow 的情況發生。 ```c static inline int mod7(uint32_t x) { x = (x >> CCCC) + (x & UINT32_C(0x7FFF)); //CCCC = 15 /* Take reminder as (mod 8) by mul/shift. Since the multiplier * was calculated using ceil() instead of floor(), it skips the * value '7' properly. * M <- ceil(ldexp(8/7, 29)) */ return (int) ((x * UINT32_C(0x24924925)) >> 29); } ``` ### 測驗三