# 2025q1 Homework4 (quiz3+4) contributed by < `wurrrrrrrrrr` > ## 測驗 1 ### 程式碼運作原理 首先看到 `ceil_div` 是無條件進位除法: ```c static size_t ceil_div(size_t n, size_t d) { return AAAA; } ``` 他的作用為計算 n 除以 d,其中 n 可以表示為 `q × d + r`,其中 r 是餘數,範圍為 0 到 d-1。若 `r == 0`,表示整除,則結果為 q;若 `r > 0`,則需向上取整,結果為 `q + 1`,為了達成這個向上取整的效果,關鍵是觀察當 n 沒有整除時,餘數 r 的範圍會落在 1 到 d - 1。因此,而如果餘數為 1 時想進位 n 就要加 d-1 ,而如果餘數為 `d - 1` 時想進位 n 只要加 1,為了涵蓋所有可能的餘數,我們選擇固定補上 d - 1,也就是使用公式 `(n + d - 1) / d`。`d - 1` 這個數值雖然在某些情況下可能超過進位所需要的,但它不會導致進位超過一位,而且又能保證所有餘數大於 0 的情況下都能正確進位。 `AAAA` = `(n+d-1)/d` 第二個 ` mpi_set_u64()` 這個函式的作用是把 `uint64_t` 拆解成多個 `31` bit 並儲存在 `mpi_t` 結構中。一開始計算要幾個 `31` bit來表示 `64` bit 的數值,而第二段是確保 rop 有足夠空間來放這些 `31` bit,如果記憶體不夠,會擴展它,再來執行了一段迴圈把 `64` bit 的整數 op 拆成 `31` bit 的小塊儲存到 rop->data[] 中,從低位到高位依序填入,而要如何拿出 31 bit?此時就需要一個 mask 只取後 31 位,因此 `INTMAX = 0x7fffffff`。 若記憶體不夠,會分配或擴展。 ```c #define INTMAX BBBB void mpi_set_u64(mpi_t rop, uint64_t op) { size_t capacity = ceil_div(64, 31); mpi_enlarge(rop, capacity); for (size_t n = 0; n < capacity; ++n) { rop->data[n] = op & INTMAX; op >>= 31; } for (size_t n = capacity; n < rop->capacity; ++n) rop->data[n] = 0; } ``` `BBBB` = `0x7fffffff` mpi_mul_naive 函式的作用是計算兩個 op1 × op2 的乘積,並將結果儲存到 rop 中。 ```c static void mpi_mul_naive(mpi_t rop, const mpi_t op1, const mpi_t op2) { ... 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]; uint64_t c = 0; for (size_t k = CCCC; c || r; ++k) { if (k >= tmp->capacity) mpi_enlarge(tmp, tmp->capacity + 1); tmp->data[k] += (r & INTMAX) + c; r >>= 31; c = tmp->data[k] >> 31; tmp->data[k] &= INTMAX; } } } ... } ``` 其中的這段模仿我們手動筆算乘法的方式: 1. 對每一位做乘法 1. 加總 1. 處理進位 而其中 `op1[n] × op2[m]` 的結果會放在第 `n + m` 位開始,就像十進位乘法的位移,因此 `CCCC` = `n + m` `mpi_fdiv_qr` 的作用是執行整數的整除與餘數運算,也就是計算 `q = n / d` 和 `r = n % d`,並將結果分別儲存在 q 和 r 中: ```c void mpi_fdiv_qr(mpi_t q, mpi_t r, const mpi_t n, const mpi_t d) { ... size_t start = mpi_sizeinbase(n0, 2) - 1; for (size_t i = start; i != (size_t) -1; --i) { mpi_mul_2exp(r, r, DDDD); 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); } } ... } ``` 例子:當 n 為 1101 時 一開始: * 商 q = 0 * 餘數 r = 0 n 是 1101 ,最高位是第 3 位 每個步驟: * 第 1 步:i = 3 * r <<= 1 ➔ r = 0 * 把 n 第 3 位(1)加進 r ➔ r = 1 * 比較 r = 1 跟 d = 3 ➔ 1 < 3,所以不用減。 * 商 q 第 3 位寫0。 第 2 步:i = 2 * r <<= 1 ➔ r = 2 * 把 n 第 2 位(1)加進 r ➔ r = 3 * 比較 r = 3 跟 d = 3 ➔ 3 == 3,可以減 * 減完 r = 0 * 商 q 第 2 位寫 1。 第3步:i=1 * r <<= 1 ➔ r = 0 * 把 n 第 1 位(0)加進 r ➔ r還是0 * 比較 r = 0 跟 d = 3 ➔ 0 < 3,不用減。 * 商 q 第 1 位寫 0。 第4步:i = 0 * r <<= 1 ➔ r = 0 * 把 n 第 0 位(1)加進 r ➔ r = 1 * 比較 r = 1 跟 d = 3 ➔ 1 < 3,不用減。 * 商 q 第 0 位寫 0。 最後結果: * 商 q = 0100 (十進位就是4) * 餘數 r = 1 * 所以 13 ÷ 3 = 4 餘 1 mpi_mul_2exp(r, r, DDDD)這行的作用是將餘數 r 左移 1 位,因此`DDDD` = `1`。 mpi_gcd 這個函式實作的是輾轉相除法來計算兩個整數 op1 和 op2 的最大公因數。依照輾轉相除法的原理: 若 b ≠ 0,則 GCD(a, b) = GCD(b, a mod b) 所以,在遞迴的情況下,只要 op2 不為 0,就持續以 op1 % op2 的餘數作為新的下一輪的參數,直到餘數等於 0 為止。 `EEEE` = `mpi_gcd(rop, op2, r)` ### 這項實作依賴遞迴呼叫,請避免使用並降低記憶體開銷 下列這段程式為 mpi_gcd 的非遞迴實作版本: ```c void mpi_gcd(mpi_t rop, const mpi_t op1, const mpi_t op2) { mpi_t a, b, q, r; mpi_init(a); mpi_init(b); mpi_init(q); mpi_init(r); mpi_set(a, op1); mpi_set(b, op2); while (mpi_cmp_u32(b, 0) != 0) { mpi_fdiv_qr(q, r, a, b); mpi_set(a, b); mpi_set(b, r); } mpi_set(rop, a); mpi_clear(a); mpi_clear(b); mpi_clear(q); mpi_clear(r); } ``` :::success 延伸問題: 解釋上述程式碼運作原理 這項實作依賴遞迴呼叫,請避免使用並降低記憶體開銷 改進最大公因數的運算效率 ::: ## 測驗 2 ### 解釋上述程式碼運作原理 ```c static inline uint64_t bit_compound(uint32_t x, uint32_t y) { return ((0L + x) << 32) | ((y + 0L) & (-1L >> 32)); } ``` 在測驗 2 中可以發現,這個函式中的` ((y + 0L) & (-1L >> 32)) `是多餘的。 原因是,我傳入的 y 是` uint32_t `類型(無號整數), 在進行 (y + 0L) 時,C 語言會將 y 進行無號升型(promotion), 轉換成 long 型別,並且高位自動補 0,不會產生符號擴展或高位污染的問題。 為了確認這段我做了測試: ```c int32_t si = -1; uint32_t ui = 0xFFFFFFFF; long sl = si + 0L; long ul = ui + 0L; printf:"Signed int32_t si = %d\n", si); printf("After (si + 0L): 0x%016lx\n", (unsigned long)sl); printf("\n"); printf("Unsigned int32_t ui = %u\n", ui); printf("After (ui + 0L): 0x%016lx\n", (unsigned long)ul); ``` 而印出來的結果為 ``` Signed int32_t si = -1 After (si + 0L): 0xffffffffffffffff Unsigned int32_t ui = 4294967295 After (ui + 0L): 0x00000000ffffffff ``` 後面的 & (-1L >> 32) 遮罩操作實際上並沒有任何作用,可以直接省略。 另外,如果真的需要截取後32位元,為了保證正確性與跨平台行為, 應該明確使用 unsigned long 型別進行操作,例如遮罩 0xFFFFFFFFUL, 而不是使用 (-1L >> 32) 這種有號數右移行為的寫法。 為了驗證上述這段我做了以下測試: ```c unsigned long ul = (unsigned long)-1L; printf("Test shifting -1L (signed long)\n"); printf("-1L >> 32 = 0x%016lx\n", (-1L >> 32)); printf("\nTest shifting (unsigned long)-1L\n"); printf("(unsigned long)-1L >> 32 = 0x%016lx\n", ul >> 32); return 0; ``` 此為輸出結果: ``` Test shifting -1L (signed long) -1L >> 32 = 0xffffffffffffffff Test shifting (unsigned long)-1L (unsigned long)-1L >> 32 = 0x00000000ffffffff ``` 而 memchr_opt 分為三個階段,其中要填入的為第二段 第一段:逐字元搜尋 c 直到記憶體位置對齊 第二段:快速以 long 單位搜尋是否包含目標字元 c 第三段:逐字元搜尋剩餘字元是否包含目標字元 c 首先看到這段: ``` #define DETECT_CHAR(X, mask) DETECT_NULL(AAAA) ``` 從題目的敘述可以知道 DETECT_NULL(X) 會檢查 X 中是否有 byte 是 0x00。 我們想判斷某個 long 變數是否包含特定字元 c,做法是: 將該 long 變數 X 與一個由目標字元重複填滿的 mask 做 XOR,若 X 中有任何 byte 等於目標字元,則 XOR 結果對應 byte 就會是 0,等同 DETECT_NULL 可檢測到,因此 `AAAA = X^mask` 由上述可得知,mask 是由目標字元 d 重複填滿,而下列這段是想產生一個 unsigned long mask,裡面所有的 byte 都填滿字元 d。 初始時先用 16 bits 來填滿(d << 8 | d),接著利用位移與 OR 操作將其擴展到 32 bits。 如果 unsigned long 是 64 位元,會執行下面的迴圈,持續將目前的 mask 左移並 OR 到自己,直到整個 64-bit mask 都被填滿: ```c unsigned long mask = d << 8 | d; mask |= mask << 16; for (unsigned int i = 32; i < LBLOCKSIZE * 8; i <<= 1) mask |= BBBB; ``` 因此可以得知 `BBBB = mask<<i` 而 CCCC 則是指向資料區塊的 unsigned long 變數,因此 `CCCC = *asrc`,用來進行目標字元的檢測 :::success 延伸問題: 解釋上述程式碼運作原理 比較 Linux 核心原本 (與平台無關) 的實作和 memchr_opt,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響 研讀 2022 年學生報告,在 Linux 核心原始程式碼找出 x86_64 或 arm64 對應的最佳化實作,跟上述程式碼比較,並嘗試舉出其中的策略和分析 下一步:貢獻程式碼到 Linux 核心 Phoronix 報導 ::: ## 測驗 3 AAAA = ENV_RUNNABLE BBBB = attempts+1 CCCC = coro_schedule DDDD = coro_yield ## 測驗 1 ```c uint32_t crc32_u8(uint32_t crc, uint8_t v) { crc ^= v; static const uint32_t crc32_table[] = { 0x00000000, 0x105ec76f, 0x20bd8ede, 0x30e349b1, 0x417b1dbc, 0x5125dad3, 0x61c69362, 0x7198540d, 0x82f63b78, 0x92a8fc17, 0xa24bb5a6, 0xb21572c9, AAAA, BBBB, CCCC, DDDD, }; crc = (crc >> 4) ^ crc32_table[crc & 0x0F]; crc = (crc >> 4) ^ crc32_table[crc & 0x0F]; return crc; } ``` ``` 給一個 0到 15 的初始值 ↓ 做 4 次 CRC 模擬(每次根據最低 bit 決定要不要 XOR) ↓ 得到最終 CRC 表格值 ↓ 填到你的查表陣列 ``` 用 0xC 當例子 初始: crc = 0x0000000C //(binary 1100) 0x0000000C 這樣的初始值,**其高位元可以假設為 0**,原因是後續的運算中,每次都會進行 crc >> 4 的位移操作,把高位資料不斷移除掉,因此原本高位的內容不會影響最終的結果。 **第一次位移 (bit 0)** * crc & 1 == 0(最低位是0) 只右移 >> crc = 0x00000006 **第二次位移 (bit 1)** * crc & 1 == 0 再右移 >> * crc = 0x00000003 **第三次位移 (bit 2)** * crc & 1 == 1 先右移,再 XOR 0x82F63B78 crc = 0x82F63B79 **第四次位移 (bit 3)** crc & 1 == 1 再次先右移再 XOR 0x82F63B78: **crc = 0xC38D26C4** 以此類推 `AAAA`:`0xc38d26c4` `BBBB`:`0xd3d3e1ab` `CCCC`:`0xe330a81a` `DDDD`:`0xf36e6f75` :::success 延伸問題: 在 Linux 核心原始程式碼中找到 CRC32 的應用案例至少 3 處,並探討其原始程式碼 設計實驗來分析 Fast CRC32 提出的手法 (要涵蓋 branchless 和查表的變形),並與硬體的 CRC32 指令效能進行比較。 以 x86-64 或 Arm64 為探討標的,說明 Linux 核心如何使用硬體加速的 CRC32 指令 ::: ## 測驗 2 `AAAA`:0x7FFF `BBBB`:0x80000000 `CCCC`:0x7FFFFFFF `DDDD`:0x80000000 `EEEE`:0x7FFFFFFF `FFFF`:input & 0xFF `GGGG`:Q15_MAX `HHHH`:Q15_MIN :::success 延伸問題: 解釋上述程式碼運作原理,搭配「信號與系統」教材 探討定點數的使用和其精確度 改進 MIDI 一閃一閃亮晶晶的音效輸出 ::: ## 測驗 3 `IIII`: max_fd `JJJJ`: &rfds 這支 server 程式是一個多人聊天室伺服器,支援同時多個 client 連線進來聊天,每當有一個 client 發訊息給 server,server 會自動把這個訊息轉發給其他所有連線中的 client,而且如果有 client 離開,server 也會自動偵測並清除連線。 執行這支程式,server 在本機的你輸入的 port 上開始監聽,等待 client 連進來。 **client 可以用像是 telnet 或 netcat 來連線到 server** ,例如: ``` telnet 127.0.0.1 5000 ``` **每成功一個連線,server 端會:** 1. 執行 accept() 接受這個新連線 1. 打印出來一行訊息,顯示 new_fd 和 client 的 IP/port 1. 把這個新 client 的 fd 記錄到 conns[] 陣列中(活躍的 client 列表) 1. 把這個 fd 加到 select() 監控的集合裡(下一輪重建 fd_set 時加進去) 1. 把這個 socket 設成 non-blocking 模式(避免 read() 卡住) ``` listening on port 5000 [4] connect from 127.0.0.1:59490 ``` **client 發送可以訊息給 server** client 任意打字,例如: Hello everyone! server 端在下一輪 select() 中偵測到這個 client fd 有資料可以讀。 ``` listening on port 5000 [4] connect from 127.0.0.1:59490 [4] activity [4] read: Hello everyone! ``` **server 處理收到的訊息** 當 server 偵測到某個 client 有資料: 呼叫 read(fd, buf, sizeof(buf)) 讀取資料。 如果 read() 回傳 > 0,代表真的有資料收到。 接著 server 會: 1. 把剛收到的這段文字存在 buf 中。 1. 迴圈遍歷所有活著的 client_fd。 1. 把這份資料(buf)寫給除了自己以外的所有 client (dest_fd != fd)。 1. 用 write(dest_fd, buf, nread) 把訊息廣播出去。 **client 斷線時的處理** 如果某個 client: 自己正常關閉連線或是網路問題中斷 server 端在 read(fd, buf, sizeof(buf)) 時會遇到: read() 回傳 0 這時 server: 1. 印出 [fd] closed 1. close(fd) 把這個 client 的連線關掉 1. conns[fd] = 0 從活躍連線列表清掉 ``` [4] activity [4] closed ``` :::success 延伸問題: 解釋上述程式碼運作原理 學習 jserv/ttt 程式碼,在不使用 (n)curses 一類現成的函式庫前提下,改進網路聊天室的文字呈現介面 在既有的程式碼之上,實作帳號登入 (login) 及基本管理功能,並該有線上與否的提示,留意 ping value 的處理機制 :::