# 2024q1 Homework4 (quiz3+4) contributed by < `aa860630`> ## 第 3 週測驗題 ### 測驗一 #### <版本一> 先是使用 $log$ 來找最高有效位數(```a```),判斷 ```result + a``` 平方是否小於等於 N 的同時,不斷透過位移 ```a``` 與運算 ```result+a``` 的方式逼近 N ```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 = 36 為例 : (下方表示方法來自 [kevinzxc1217](https://hackmd.io/ZHWqRDtCRXCWcohnfdyWxw?view) ) | 迭帶次數 | a | result | (result + a) * (result + a) <= N) |result+=a| |:-------------------------:|:-----:|:---------------:|:------------:|:------------:| | 0 | 32 | 0 | False |0| | 1 | 16 | 0 | False |0| | 2 | 8 | 0 | False |0| | 3 | 4 | 0 | True |$2^2$| | 4 | 2 | 4 | True |$2^2$+$2^1$| | 5 | 1 | 6 | False |$2^2$+$2^1$+$2^0$| #### <版本二> 方法與<版本一>一致,差別為使用 ```while``` 迴圈來找到最高有效位元 #### <版本三> ### 測驗二 在程式開發中,除法運算的複雜度高,對於硬體的限制也極高,因此我們利用加法與乘法來取代除法,在提高程式效能的同時,在某些情況下可以提供更好的精確度和硬體實現的簡單性 ### 測驗三 #### <版本一> 透過不斷右移來判斷 i 是否為零,為了找到最高有效位元 #### <版本二> 按照順序判斷 ```i``` 是否大於 $2^{16}$、$2^{8}$、$2^{4}$、$2^{2}$,若有則將該冪加到 ```result``` 並將 ```i``` 右移相對應的冪,最後得出的 ```result``` 為最高有效位元 ```c static size_t ilog2(size_t i) { size_t result = 0; while (i >= 65536) { result += 16; i >>= 16; } while (i >= 256) { result += 8; i >>= 8; } while (i >= 16) { result += 4; i >>= 4; } while (i >= 2) { result += 1; i >>= 1; } return result; } ``` 在<版本一>程式碼中,無論輸入的數值是多少位元,處理的時間都是與該數值的位元數成正比的。因此,如果最高有效位元是最高位元,則需要處理整個32位元或64位元 然而,在<版本二>程式碼中,根據數值的大小,處理的時間會根據不同的條件而有所不同。當輸入的數值足夠大時,透過逐步將數值除以較大的數(例如65536、256、16),可以快速地減小數值,因此在處理時間上會更有效率。這種方法類似於對數運算,每次除以一個較大的數相當於對數運算中的一次除法。因此,在一定的情況下,<版本二>程式碼只需處理較少的位元,其時間複雜度可以表示為 $O(\log n)$,其中 $n$ 是輸入的數值。 #### <版本三> 在 [GCC, THE GNU Compiler Collection](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)中提到 GCC 提供一系列的 built-in 函數用來優化編譯結果,這些函數以```__builtin_```作為前綴,以下介紹一些 built-in 函數及其作用 * ```__builtin_clz(int x)``` : 傳回x中前導 0 位的數量(從最高有效位元位置開始)。如果x為 0,則結果未定義 * ``` __builtin_ctz(int x)``` : 傳回x中尾隨 0 位的數量(從最低有效位位置開始)。如果x為 0,則結果未定義 * ``` __builtin_ffs(int x)``` : 返回 x 中最後一個為 1 的位數 最高位元減掉 ```v``` 的前導 0 位的數量即為最高有效位元,程式碼如下: ```c int ilog32(uint32_t v) { return (31 - __builtin_clz(v | 1)); } ``` 在 ```__builtin_clz()``` 函數中引數若直接為 ```v``` ,多數情況下是對的,但由於此函數在 ```v``` 為0的情況下結果未被定義,因此該處應為``` v | 1``` ### 測驗四 ```c /* Exponentially weighted moving average (EWMA) */ struct ewma { unsigned long internal; unsigned long factor; unsigned long weight; }; ``` * ```internal```: 是用來儲存內部表示的加權移動平均值,會隨著新的輸出被加到 moving average 中而更新 * ```factor```: 用來表示 internal 的縮放倍率,掌控 internal 的精度。 * ```weight```: 代表輸出的權重,不同 weight 表示輸出對整體平均影響的大小。通常會被設定為 2 的冪使得計算有效率 ```c /** * 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_int```在資料初始化的過程順勢將```avg->weight```與```avg->factor ```都取了 $log2$,方便在之後透過位移```avg->weight```或```avg->factor```的方式達到更好的運算效果 根據 [Exponentially Weighted Moving Average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) 得出以下公式 : $$S_t = \left\{ \begin{array}{l} Y_0& t = 0 \\ \alpha Y_t + (1 - \alpha)\cdot S_{t-1}& t > 0 \\ \end{array} \right.$$ 在```ewma_add```函式中,首先判斷```avg->internal```是否有值,若```avg->internal```不為0,則進行以下操作(為了方便解釋,所有參數皆省略前綴```avg->```): **func1** : internal = (((internal << weight) - internal) + (val << factor)) >> weight 為了更好的與上述公式做連結我們粗魯地將上述運算再簡化成以下 : **func2**: internal = ((internal - (internal >> weight)) + (val << factor) >> weight) 其中```weight```對應到 $\alpha$,```val << factor```對應到 $Y_t$ 既然可以用更吻合公式的 **func2** 來編寫,為何此處會使用 **func1** 的方式? 是因為在進行 bit 右移運算時可能會導致最右邊的 bit 移失,導致精準度下降,事先左移 bits 可以改善這個問題 ``` diff struct ewma *ewma_add(struct ewma *avg, unsigned long val) { avg->internal = avg->internal - ? (((avg->internal << EEEE) - avg->internal) + + ? (((avg->internal << avg->weight) - avg->internal) + - (val << FFFF)) >> avg->weight + (val << avg->factor)) >> avg->weight : (val << avg->factor); return avg; } ``` ### 測驗五 ```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 減1。這是因為如果 x 已經是2的冪次方,我們想要得到的 r 應該是 x 的對數,而不是對數加1。舉例來說,如果 x 是4(即`2^2`),我們想要得到的 r 是2。如果 x 是3或者4,減去1之後都會得到2,然後我們會找到2的對數是 1,最後函數會返回 1 + 1 = 2 2. 根據數值大小逐步縮小範圍,同時累計需要的 bit 移位數來表示對數 r, `r = (x > 0xFFFF) << 4;`: 判斷 x 是否大於`0xFFFF`(即`65535`)。如果是,`r` 被設置為`16`,否則為`0`。 3. 下面的步驟重複上述過程,但範圍更小,並逐步累積 r 的值: - `shift = (x > 0xFF) << 3;`: 檢查 x 是否大於 `255`(`0xFF`),如果是,shift 被設置為 `8`,否則為 `0`。 - `x >>= shift`: 根據 shift 的結果,進一步右移 x。 - `r |= shift`: 使用位元或運算(`|`),將 shift 的結果累加到 r 上 4. 最後一行 `return (r | shift | x > 1) + 1;`: - `r | shift`:將 r 和最後的 shift 結果進行位元或運算。 - `x > 1`:如果經過所有的右移之後 x 大於1,表示還需要額外加1到結果中。 - 最後,將這些結果加起來,並且加 1(因為我們在開始時將 x 減了1,現在需要把這個減掉的1加回來)。 #### 在 Linux 核心原始程式碼類似於```ceil_ilog2()```實作 [linux/tools/testing/selftests/mm /thuge-gen.c](https://github.com/torvalds/linux/blob/master/tools/testing/selftests/mm/thuge-gen.c#L51) 函式通過不斷地左移 1 來找到給定數字,當 ```(1UL << l)``` 大於或等於給定的```v```時,迴圈停止運行。這意味著找到了最小的```l```值,此時的```l```即為原程式碼```ceil_ilog2(v)```的結果 ```c int ilog2(unsigned long v) { int l = 0; while ((1UL << l) < v) l++; return l; } ``` Huge page 是作業系統中使用的一種記憶體管理技術。它們比標準記憶體分頁更大,通常是 2MB 或更大。 Huge page 的主要目的是提高記憶體管理效率和系統性能。在傳統的記憶體中,作業系統為每個進程維護一個 page,將 virtual address 映射到 pysical address。當存在大量的小 pages 時,頁表可能變得非常大,導致性能下降,因為作業系統花費更多時間在管理它們上。Huge page 減少了映射給定記憶體範圍所需的頁表項目數量,從而提高了效率。 ```c #define SHM_HUGE_SHIFT 26 for (i = 0; i < num_page_sizes; i++) { unsigned long ps = page_sizes[i]; int arg = ilog2(ps) << MAP_HUGE_SHIFT; ksft_print_msg("Testing %luMB mmap with shift %x\n", ps >> 20, arg); test_mmap(ps, MAP_HUGETLB | arg); } ``` * 透過```ilog2(ps)```計算出每個 pages 大小,假設 pages 大小為 2MB,我們知道 $2^{21}B = 2MB$,因此其對數為 21 * ```SHM_HUGE_SHIFT```預設為 26,```<< MAP_HUGE_SHIFT```的目的是確保 huge page 大小可以正確地對應到虛擬記憶體的地址空間。 * ```test_mmap```函式用於測試在指定條件下是否能夠成功使用 huge page 進行內存映射