# 2025q1 Homework4 (quiz3+4) contributed by < `liangchingyun` > ## 2025q1 第 3 週測驗題 ### 測驗 [`1`](https://hackmd.io/@sysprog/linux2025-quiz3#%E6%B8%AC%E9%A9%97-1) 實作一個多精度整數的運算框架,也就是支援比 64-bit 更大的整數計算 #### 延伸問題 1 > 解釋上方程式碼運作原理 ##### 資料結構定義 ```c #include <inttypes.h> #include <stdint.h> /* mpi: Multi-Precision Integers */ typedef struct { uint32_t *data; // 存放多精度整數每一部分的陣列,每個元素為 31-bit size_t capacity; // 表示 data 陣列目前有多少個 element (即有多少「31-bit 位元段」) } mpi_t[1]; typedef size_t mp_bitcnt_t; ``` ##### 記憶體管理 ```c // 初始化 void mpi_init(mpi_t rop) { rop->capacity = 0; rop->data = NULL; } // 釋放記憶體 void mpi_clear(mpi_t rop) { free(rop->data); } // 擴展 data 陣列的長度,並初始化新擴展的空間為 0 void mpi_enlarge(mpi_t rop, size_t capacity) { if (capacity > rop->capacity) { // 只有在新的 capacity 大於目前的容量時,才需要實際擴充 size_t min = rop->capacity; rop->capacity = capacity; rop->data = realloc(rop->data, capacity * 4); // 新的空間大小是 capacity * 4 bytes // 原因是data 裡面每個元素是 uint32_t,其大小就是 4 bytes if (!rop->data && capacity != 0) { fprintf(stderr, "Out of memory (%zu words requested)\n", capacity); abort(); } // 對新分配的區塊(從舊的大小 min 到現在的大小 capacity)進行初始化 for (size_t n = min; n < capacity; ++n) rop->data[n] = 0; } } // 去除尾端為 0 的位元段,減少記憶體使用 void mpi_compact(mpi_t rop) { size_t capacity = rop->capacity; // 如果目前容量是 0,直接結束,無需處理 if (rop->capacity == 0) return; // 找出最右邊(高位)非零的那一個元素 for (size_t i = rop->capacity - 1; i != (size_t) -1; --i) { if (rop->data[i]) break; capacity--; // 若 rop->data[i] == 0,代表這一格可以被移除,因此縮減 capacity } // 保護性檢查 assert(capacity != (size_t) -1); // 重新配置記憶體,只保留實際有用的部份 rop->data = realloc(rop->data, capacity * 4); rop->capacity = capacity; if (!rop->data && capacity != 0) { fprintf(stderr, "Out of memory (%zu words requested)\n", capacity); abort(); } } ``` ##### 輔助工具 ```c /* ceiling division without needing floating-point operations. */ static size_t ceil_div(size_t n, size_t d) { return (n + d - 1) / d; // 計算 n ÷ d 的向上取整(ceiling)結果 } #define INTMAX 0x7fffffff // 31-bit 的最大整數值 ``` | n | d | n / d | ceil_div(n, d) | |:---:|:---:|:-----:|:--------------:| | 10 | 3 | 3.33 | 4 | | 5 | 2 | 2.5 | 3 | | 8 | 4 | 2 | 2 | | 0 | 3 | 0 | 0 | ##### 基本設定與轉換 ```c // 將一個 64-bit 的整數,切成多個 31-bit 段,裝入 data 陣列 void mpi_set_u64(mpi_t rop, uint64_t op) { size_t capacity = ceil_div(64, 31); // 需要 3 個欄位 mpi_enlarge(rop, capacity); // 確保 rop 有足夠空間 // 拆解 uint64_t 成多個 31-bit 整數 for (size_t n = 0; n < capacity; ++n) { rop->data[n] = op & INTMAX; // 取低 31-bit op >>= 31; // 將剩下的往右移,準備下次寫入 } // 清除多餘資料 for (size_t n = capacity; n < rop->capacity; ++n) rop->data[n] = 0; } ``` * `rop`: 多精度整數,作為輸出目標。 * `op`: 一個 64-bit 的無號數,要被轉換並儲存。 * 例子: * `rop->data[0]` = 最低 31-bit * `rop->data[1]` = 中間 31-bit * `rop->data[2]` = 最高 2-bit(因為 31x3 = 93 > 64) ##### 乘法 ```c static void mpi_mul_naive(mpi_t rop, const mpi_t op1, const mpi_t op2) { size_t capacity = op1->capacity + op2->capacity; mpi_t tmp; mpi_init(tmp); mpi_enlarge(tmp, capacity); for (size_t n = 0; n < tmp->capacity; ++n) tmp->data[n] = 0; for (size_t n = 0; n < op1->capacity; ++n) { for (size_t m = 0; m < op2->capacity; ++m) { uint64_t r = (uint64_t) op1->data[n] * op2->data[m]; // r 是中間乘積(會超過 31 bit,所以用 uint64_t) uint64_t c = 0; for (size_t k = n+m; c || r; ++k) { // 從 k = n + m 開始累加(就是十進位乘法中對齊的位置) if (k >= tmp->capacity) mpi_enlarge(tmp, tmp->capacity + 1); tmp->data[k] += (r & INTMAX) + c; // 加上本位結果與前一位的進位 r >>= 31; // 下一個 31 bit c = tmp->data[k] >> 31; // 如果 overflow,產生進位 tmp->data[k] &= INTMAX; // 只保留 31 bit } } } mpi_set(rop, tmp); mpi_compact(rop); mpi_clear(tmp); } ``` 例子: 假設 `op1 = ABC`,`op2 = DE` ``` A B C × D E --------------------- AE BE CE ← 對齊加總 + AD BD CD --------------------- ``` ##### 除法與餘數 * 初始化商 `q = 0`,餘數 `r = 0` * 從最高位開始,比對每一位是否能被除數 `d` 減去 * 如果可以,商的那一位設為 `1`,餘數更新為減掉後的值 ```c void mpi_fdiv_qr(mpi_t q, mpi_t r, const mpi_t n, const mpi_t d) { // 建立副本避免破壞原始數據 mpi_t n0, d0; mpi_init(n0); mpi_init(d0); mpi_set(n0, n); mpi_set(d0, d); // 除以零檢查 if (mpi_cmp_u32(d0, 0) == 0) { fprintf(stderr, "Division by zero\n"); abort(); } // 初始化商與餘數 mpi_set_u32(q, 0); mpi_set_u32(r, 0); // 從最高位 bit 開始處理 size_t start = mpi_sizeinbase(n0, 2) - 1; // Long Division 迴圈,每次處理一個 bit for (size_t i = start; i != (size_t) -1; --i) { mpi_mul_2exp(r, r, 1); if (mpi_testbit(n0, i) != 0) mpi_setbit(r, 0); if (mpi_cmp(r, d0) >= 0) { mpi_sub(r, r, d0); mpi_setbit(q, i); } } mpi_clear(n0); mpi_clear(d0); } ``` ##### 最大公因數 * 遞迴呼叫 `mpi_fdiv_qr`,直到餘數為 `0`,得到 GCD * 終止條件:`op2 == 0`,這時 `op1` 就是 GCD。 ```c void mpi_gcd(mpi_t rop, const mpi_t op1, const mpi_t op2) { if (mpi_cmp_u32(op2, 0) == 0) { mpi_set(rop, op1); return; } mpi_t q, r; mpi_init(q); mpi_init(r); mpi_fdiv_qr(q, r, op1, op2); mpi_gcd(rop, op2, r); mpi_clear(q); mpi_clear(r); } ``` ##### 測試程式碼 驗證 `549755813889 ÷ 1234 = 445507142...661` 是否正確。 ```c printf("mpi_fdiv_qr\n"); { mpi_t n, d, q, r; mpi_init(n); mpi_init(d); mpi_init(q); mpi_init(r); mpi_set_str(n, "549755813889", 10); mpi_set_str(d, "1234", 10); mpi_fdiv_qr(q, r, n, d); assert(mpi_cmp_u32(q, 445507142) == 0); assert(mpi_cmp_u32(r, 661) == 0); mpi_clear(n); mpi_clear(d); mpi_clear(q); mpi_clear(r); } ``` 驗證 `GCD(2310, 46189) = 11` ```c printf("GCD test\n"); { mpi_t a, b, r; mpi_init(a); mpi_init(b); mpi_init(r); mpi_set_str(a, "2310", 10); mpi_set_str(b, "46189", 10); mpi_gcd(r, a, b); assert(mpi_cmp_u32(r, 11) == 0); mpi_clear(a); mpi_clear(b); mpi_clear(r); } ``` #### 延伸問題 2 > 這項實作依賴遞迴呼叫,請避免使用並降低記憶體開銷 #### 延伸問題 3 > 改進最大公因數的運算效率 ### 測驗 [`2`](https://hackmd.io/@sysprog/linux2025-quiz3#%E6%B8%AC%E9%A9%97-2) #### 延伸問題 1 > 解釋上述程式碼運作原理 ##### 判斷一個整數是否為奇數 確認最低位元是否為 1 傳統寫法: ```c #include <stdint.h> bool both_odd(uint32_t x, uint32_t y) { return (x & 1) && (y & 1); } ``` 這樣需要兩次 `&` 運算與一次`&&` 邏輯運算。 SWAR 的核心思想是:把多個資料打包在同一個暫存器中,並一次性處理,這可以減少操作次數並提升效能。 bitmask 定義: ```c static uint64_t SWAR_ODD_MASK = (1L << 32) + 1; // 第 0 位及第 32 位為 1 bool both_odd_swar(uint64_t xy) { return (xy & SWAR_ODD_MASK) == SWAR_ODD_MASK; } ``` 測試程式: ```c static inline uint64_t bit_compound(uint32_t x, uint32_t y) // 同時檢查兩個位置的最低位是否都是 1 { return ((0L + x) << 32) | ((y + 0L) & (-1L >> 32)); } int main() { int x = 12345678; int y = 9012345; uint64_t xy = bit_compound(x, y); printf("%d, %d\n", both_odd(x, y), both_odd_swar(xy)); } ``` ##### 加速 `memchr` 搜尋 步驟: 1. 對齊檢查:先用 byte-wise 判斷,直到指標對齊。 2. word 級搜尋:一次檢查 `sizeof(long)` bytes,判斷這個 word 內是否有目標字元。 3. 當剩下不足一整個 word 時,再回到 byte-wise 搜尋。 ```c #include <limits.h> #include <stddef.h> #include <stdint.h> #include <string.h> /* Nonzero if X is not aligned on a "long" boundary */ #define UNALIGNED(X) ((long) X & (sizeof(long) - 1)) /* How many bytes are loaded each iteration of the word copy loop */ #define LBLOCKSIZE (sizeof(long)) /* Threshhold for punting to the bytewise iterator */ #define TOO_SMALL(LEN) ((LEN) < LBLOCKSIZE) #if LONG_MAX == 2147483647L #define DETECT_NULL(X) (((X) - (0x01010101)) & ~(X) & (0x80808080)) #else #if LONG_MAX == 9223372036854775807L /* Nonzero if X (a long int) contains a NULL byte. */ #define DETECT_NULL(X) \ (((X) - (0x0101010101010101)) & ~(X) & (0x8080808080808080)) #else #error long int is not a 32bit or 64bit type. #endif #endif /* @return nonzero if (long)X contains the byte used to fill MASK. */ #define DETECT_CHAR(X, mask) DETECT_NULL(X^mask) void *memchr_opt(const void *str, int c, size_t len) { const unsigned char *src = (const unsigned char *) str; unsigned char d = c; while (UNALIGNED(src)) { if (!len--) return NULL; if (*src == d) return (void *) src; src++; } if (!TOO_SMALL(len)) { /* If we get this far, len is large and src is word-aligned. */ /* The fast code reads the source one word at a time and only performs * a bytewise search on word-sized segments if they contain the search * character. This is detected by XORing the word-sized segment with a * word-sized block of the search character, and then checking for the * presence of NULL in the result. */ unsigned long *asrc = (unsigned long *) src; unsigned long mask = d << 8 | d; mask |= mask << 16; for (unsigned int i = 32; i < LBLOCKSIZE * 8; i <<= 1) mask |= mask<<i; while (len >= LBLOCKSIZE) { if (DETECT_CHAR(*asrc, mask)) break; asrc++; len -= LBLOCKSIZE; } /* If there are fewer than LBLOCKSIZE characters left, then we resort to * the bytewise loop. */ src = (unsigned char *) asrc; } while (len--) { if (*src == d) return (void *) src; src++; } return NULL; } ``` #### 延伸問題 2 > 比較 Linux 核心原本 (與平台無關) 的實作和 `memchr_opt`,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響 > #### 延伸問題 3 > 研讀 [2022 年學生報告](https://hackmd.io/@arthur-chang/linux2022-quiz8),在 Linux 核心原始程式碼找出 x86_64 或 arm64 對應的最佳化實作,跟上述程式碼比較,並嘗試舉出其中的策略和分析 > 下一步:貢獻程式碼到 Linux 核心 > [Phoronix 報導](https://www.phoronix.com/news/Linux-Kernel-Faster-memchr) ### 測驗 [`3`](https://hackmd.io/@sysprog/linux2025-quiz3#%E6%B8%AC%E9%A9%97-3) ## 2025q1 第 4 週測驗題 ### 測驗 [`1`](https://hackmd.io/@sysprog/linux2025-quiz4#%E6%B8%AC%E9%A9%97-1) #### 延伸問題 1 > 在 Linux 核心原始程式碼中找到 CRC32 的應用案例至少 3 處,並探討其原始程式碼 #### 延伸問題 2 > 設計實驗來分析 [Fast CRC32](https://create.stephan-brumme.com/crc32/) 提出的手法 (要涵蓋 branchless 和查表的變形),並與硬體的 CRC32 指令效能進行比較。 #### 延伸問題 3 > 以 x86-64 或 Arm64 為探討標的,說明 Linux 核心如何使用硬體加速的 CRC32 指令 ### 測驗 [`2`](https://hackmd.io/@sysprog/linux2025-quiz4#%E6%B8%AC%E9%A9%97-2) #### 延伸問題 1 > 解釋上述程式碼運作原理,搭配「[信號與系統](https://ocw.aca.ntu.edu.tw/ntu-ocw/ocw/cou/103S207)」教材 #### 延伸問題 2 > 探討定點數的使用和其精確度 #### 延伸問題 3 > 改進 MIDI 一閃一閃亮晶晶的音效輸出 ### 測驗 [`3`](https://hackmd.io/@sysprog/linux2025-quiz4#%E6%B8%AC%E9%A9%97-3) #### 延伸問題 1 > 解釋上述程式碼運作原理 #### 延伸問題 2 > 學習 [jserv/ttt](https://github.com/jserv/ttt) 程式碼,在不使用 (n)curses 一類現成的函式庫前提下,改進網路聊天室的文字呈現介面 #### 延伸問題 3 > 在既有的程式碼之上,實作帳號登入 (login) 及基本管理功能,並該有線上與否的提示,留意 [ping value](https://www.business.att.com/resources/knowledge-center/what-is-latency-impact-on-gaming-streaming.html) 的處理機制