contributed by <Andrushika
>
本題使用 mpi_t
結構,作為包裝大數的基礎資料結構,結構定義如下:
其中 data
會是儲存多個 32 bits (實際上是 31 bits) 整數的 array,capacity
則代表 array 的大小。注意到最終定義 mpi_t[1]
,屬於一種小技巧,可以用來模擬物件導向風格的程式。
若要使用 mpi_t
,在不使用上述技巧的情況下,會需要做以下操作進行初始化:
但使用上述技巧後,mpi_t
被定義為給定 struct
的陣列,所以記憶體空間已經分配好。使用 mpi_t
的方式就可以簡化:
可以發現 number
就像 OOP 中包裝好的 object 一樣,mpi_t
就像某個 class。
觀察 ceil_div
,基本的除法向上取整操作:
從先前定義的 mpi_t
可以發現 data
的基本單位應該為 32 bits signed int,但在題目的 context 下,mpi 只支援無號數運算,可以從以下 function 觀察出來:
以上程式碼會以 31 bits 為單位儲存數值,因此,可以求得 BBBB
的答案:
mpi_mul_naive
使用類似直式乘法的概念來實作,每次相乘一位後持續往高位數進位。
r
代表剩餘需處理進位的數,c
表示 carry。
可以推得 CCCC
需填入 n + m
,以對齊直式乘法操作時正確的位置。
mpi_fdiv_qr
的實作是使用二進位長除法的概念,維護一個 r
代表 remainder,從被除數的最高位做到最低位,每次將一位加入 remainder 中。當 r > d
代表有得除,將商的該位 bit 設為 1。
當初考試時一直卡在 mpi_mul_2exp
的作用。本函式的作用是將 mpi 乘上 2 的指數,實際上就是做左移操作。因為每次都要將被除數的一位加入 r
,r
就需要事先左移空出一位。所以 DDDD
的答案:
mpi_gcd
使用遞迴版本的輾轉相除法求最大公因數。每次求得 op1 % op2
的結果後,再用該結果進行下次的運算,因此可以填入 EEEE
:
mpi_gcd
的實作方式改為 iterativeDETECT_NULL
的巧思本 macro 被用來確認 X
之中是否存在 NULL byte:
為何這樣做有用?可以將每個 byte 分開、並拆成三種 case 來看。
因為最終會做 bitwise AND 0x80
,可以想成是對 MSB 進行檢測;接下來,關注 MSB
在不同 case 中是如何變化的。
byte = 0x00
在 (X) - (0x0101010101010101)
中:0x00
- 0x01
= 0xFF
, MSB = 1
在 ~(X)
中:~0x00
= 0xFF
, MSB = 1
因為兩個 MSB 都被 set,最後檢測就會被判定為 NULL byte
byte
介於 0x01
到 0x7F
在 (X) - (0x0101010101010101)
中:X
- 0x01
, MSB = 0
因此,無論後續做什麼操作,都不會被判定為 NULL byte
byte
介於 0x80
到 0xFF
在 ~(X)
中:MSB = 0
因此,無論後續做什麼操作,都不會被判定為 NULL byte
根據 memchr_opt
的定義:
本函式會在長度為 len
的 str
中,尋找字元 c
是否存在。
DETECT_CHAR
macro 透過前面提及的 DETECT_NULL
來幫助判斷給定的 mask
是否存在。只要先將 mask
對 X
做 XOR,就可以將 target byte 變成 NULL byte,進而能用 DETECT_NULL
判斷出來。
若記憶體位置沒有和 sizeof(long)
對齊,則先一個一個字元讀取,並確定是否為目標字元,直到對齊為止。
當記憶體 ALIGNED
之後,就可以用題目所提及之 SWAR
方法快速求解。
首先需要將要尋找的目標 d
做成用來一次偵測多個 byte 的 mask
,然後再使用 DETECT_CHAR
進行偵測:
範例程式碼中實作了一個 envs[NENV]
(NENV = 1024
),目的是保留不同 coroutine 的執行狀態。
在 coro_create
之中,會創建新的 coroutine
並進入等待被排程的狀態,所以可以填入:
注意到 make_context
的用法,其將用來設定 context
相關函式執行入口點及參數:
其中 entry
是在 coro_create
中傳入的函式指標。
根據題目敘述,排程採用 round-robin 策略,所以每次會找下一個 index 的 candidates,即:
在選定 candidate 之後,就會設定一個 10 ms 的 timer,幫助後續進行 preempt,並使用 setcontext
跳回該 coroutine 先前執行到一半的狀態。
coro_yield
在讓出 CPU 之前,會先用保存 context,然後交由排程器決定下一個要執行的 coroutine,所以 BBBB = coro_schedule
。
timer 將會觸發 preempt()
,強制當前 coroutine 讓出 CPU,因此 DDDD = coro_yield
在 crc32_naive
做法中,會使用到「反向」二進位多項式除法的原理(即會從最低位開始運算,在二進位運算中,和從最高位開始會是等價的)。
參考 Fast CRC32,一開始計算的迴圈部分如下:
可以加速為 branch free 版本:
然後再額外提升效能,由於 (crc & 1) 的結果始終只有 0 或 1,可以再簡化成以下版本:
根據 Fast CRC32 所提及的 "Half-Byte Algorithm",可以用來產生剩餘的答案:
不要列出參考題解,專注題目本身
因為程式是使用離散取樣模擬 phase,要產生特定波的時候,可以想像成每次取樣都推進一點 phase。假設每秒取樣 次,而我們想產生 Hz 的音的時候,可以透過下列算式計算每次 sample 時 phase 該增加多少:
題目中使用 Q15 去表示 phase ():
對應的實作 code:
接下來是根據 node type 更新 state
的部分,此處的 osc 因為要確保 state 始終可以用 Q15 來表示 phase,所以使用模除運算處理。
這段集中在處理 envelope,先看先前取 state 值的方式:
所以對於 SYNTH_NODE_ENVELOPE
,低 23 位應該會用來存 value,然後才進行右移,目的是為了保留額外的 4 位精度。
同時註解中提到:
The highest bit in the state indicates decay mode
所以 BBBB
應填入:
CCCC
則是 0x7FFFFF
,如先前所示。
DDDD
需要在 attack 階段結束、value 達到最大值時,設置為 decay mode,所以應填入 0x00800000
。
EEEE
的作用是在 gate 關閉時、release 作用時,把 decay bit 清除掉,這樣下一次 gate 打開時才可以直接進入 attack;應填入 0xFF7FFFFF
。
sine_wave
的實作原理是,先使用 look up table 對比較大 scale 下的數值查表,查表後再用線性插值法提升精確度。
由於 input phase 是使用 q15_t
表示,且 index 讀取時先右移八位,代表最高七位會用來當 index。
先填入 FFFF
:
以上運算,可以想像是對 res 和 next 中間先計算「距離 table 的下一個點,目前走了多少百分比?」,然後用這個百分比來插值。
square_wave
也是輸入 phase,輸出對應的波。
需要在前半個週期輸出 +1
, 後半個週期輸出 -1
,因此修改如下:
本題使用 select
實作了 TCP 連線工具,有幾個變數值得注意:
server_fd
:負責處理新 socket 連線的 file descriptor。
conns[FD_SETSIZE]
:在接收到 socket 連線請求後,會得到一個連線成功後傳輸資料用的 fd。conns[fd] = 1
時代表目前已建立連線,同時會將 fd 加入 &rfds
,以便使用 select()
觀測。
此處考的是 select 的用法,參照 linux manual page:
IIII
對應的是 nfds
,說明如下:
This argument should be set to the highest-numbered file descriptor in any of the three sets, plus 1. The indicated file descriptors in each set are checked, up to this limit (but see BUGS).
因為 rfds 只有監聽 server_fd
,所以 IIII
應填入 server_fd + 1
,也就是 max_fd
。
JJJJ
對應的是 readfds
,此處應填入 &rfds
。值得注意的是,在 select()
return 之後,rfds
會被改變,只剩下已經 ready 的 fds 被 set。
關於 select 運作的疑惑:
在我的理解中,select
應該是在偵測到任何一個 file descriptor ready 時,就會馬上 return。在微觀的角度下,kernel 應該無法「同時」偵測到多個 file descriptor ready,而在偵測到任一 fds ready 就要馬上 return 的狀況下,為何會出現 rfds 中可以找到多個 FD_ISSET
的狀況?
do_select()
的工作流程以下是 do_select()
函數的簡化流程:
這段程式碼展示了 do_select()
如何初始化等待佇列,檢查描述符狀態,並在必要時進入等待狀態。
select()
可能返回多個就緒的描述符?正如您之前提到的,從物理層面來看,兩個事件不可能在完全相同的時間發生。然而,對於 select()
而言,它關心的是在一次系統呼叫期間,哪些描述符在其返回之前變成了就緒狀態。這意味著:
如果在 select()
被呼叫後,fd1
和 fd2
都在其返回之前變成了就緒狀態,則 select()
會同時報告這兩個描述符為就緒。
這種行為是因為 select()
在返回時會檢查所有關心的描述符,並將所有已就緒的描述符標記在 fd_set
中。