contributed by <csotaku0926
>
根據 digit-by-digit calculation 原理,假設欲求平方根為 ,可將 化為 2 的冪總和:
已知 ,則有以下遞迴關係
(單純看式子很難懂,可以用平方矩陣去想比較好理解)
令平方根逼近值,
即是欲求平方根
顯然
因此對於每個 ,於 加上 ,由於是 2 的冪,因此只有以下兩種可能:
,
但我們的程式並不是用這個較直白的方法判別是否加 ,而是用差值的方式
令 (考慮從 n 開始),
對於每個 m:
令我費解的是,程式又進一步將 拆解成 ,
想了很久,原來只是單純將加法的兩項拆開來看,即:
,
每次迴圈更新:
計算到最後 ,
太難理解了,單單根號運算為了追求表現,竟然會變得這麼複雜 (我資質駑鈍)
原先 (31 - __builtin_clz(x))
的部份是為了算 MSB,現在用 strings.h
提供的 ffs
達成
要理解 linux 核心程式碼並不是容易的事情,光是說明提供參考的 block 目錄就有許多檔案,要如何在其中找到平方根實作?
透過 google 關鍵字搜尋,總算是在 block/blk-wbt.c
這個檔案中找到有關平方根的動作:
由註解看出這是一段有關調整 monitor window size 的程式碼,有點像網路程式設計學到的滑動視窗 (sliding window) ,可以根據 scaling step 逐漸縮小
實際呼叫平方根的函式為 rwb_arm_timer
:
而定義 int_sqrt
的函式位於 /lib/math/int_sqrt.c
除了獲取 MBS 的方式是使用 _fls
, 將迴圈改為 while loop 之外,整體演算法是相同的
根據教師所言,這題目是希望引導學員思考針對 RV32I/RV64I 這類沒有整數乘法和除法指令的指令集,該如何撰寫有效的程式
希望以 bitwise operation 實作除法,但若除數 (這個例子中, 10) 包含非 2 的冪的因數,會有不精確的問題
項目說明 中透過推導證明,若存在 ,選定一 使 直到小數第一位都是精準的,即
則對於任何小於 n 的非負整數 l , 直到小數第一位也是精準的
考慮輸入不大於 19 的狀況下,找出能夠維持小數第一位精確度的除數 x :
–>
由於是 bitwise operation,除數的形式應該要是 ,a 是任意配對的正整數
接下來在測試程式處,有個匪夷所思的地方:
這段程式碼想要求得 13 * tmp
,但他的步驟是先求得 13 * tmp / 8
後透過左移變回 13 * tmp
,又此時因經過右移導致部份位元遭捨棄,因此又用 d0, d1, d2 補回。
為什麼不直接求 13 * tmp
就好了? 可以進一步寫成以下形式,節省變數運用:
可見測試題中的程式碼只是其中一種可行方法,不代表最優解
而包裝程式又換了一種寫法:
這一行初步認為是將除數 x 換成 ,而 q 的值指派為
接下來又是一個匪夷所思的部份:
這行程式碼重複了四次,也就是說,in 不斷加入 in >> 8
數值,若 in 是小於 256 的正整數,這些程式對 in 毫無作用。也許要結合後段程式碼才會知道這樣寫的目的
最後,q 需要再右移一個數,才是最終的商。考慮
–>
需要將 乘上適當的 2 的冪,使其符合 x 的條件
而 ,最符合上述條件,可知 *div = (q >> 3);
最後一行是在算餘式,已知 ,
q & ~0x7
相當於
所以: *mod = in - ((q & ~0x7) + (*div << 1));
項目參考
一行行看指令對照表有些費時,如果可以做個程式自動計算 cycle 應該很不錯
想要計算這部份,第一步是要確認電腦的處理器版本,我使用的型號為 Intel 12th gen Core
對照圖表,並沒有發現直接對應的微處理器,先以型號最接近的 "Nehalem" 探討
先以 gcc div10.c -o div10
產出執行檔後,以 objdump -d div10 > div10.asm
觀察反組譯的結果 (-O 是 gcc 最佳化選項,預設是 -O0 )
預設最佳化等級取決於 GNU toolchain 編譯時的選項
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
首先是以除法和模數寫成的直覺版本:
實際查看指令表格,有許多欄位。觀看敘述得知 "latency" 以 cpu cycle 為時間單位測量指令延遲,因此要對照這個欄位,以下是出現在反組譯檔案中的指令之對應 cycle 數:
指令 | cycle | count |
---|---|---|
push r | 3 | 1 |
mov r r/i | 1 | 18 |
imul | 3 | 2 |
add/sub | 1 | 3 |
shr/shl | 1 | 4 |
pop r | 2 | 1 |
佔用 CPU 週期總計 36 個
至於只使用位元運算的除法:
指令 | cycle | count |
---|---|---|
push r | 3 | 1 |
mov r r/i | 1 | 18 |
lea | 1 | 3 |
add/sub | 1 | 4 |
shr/shl | 1 | 2 |
pop r | 2 | 1 |
佔用 CPU 週期總計 32 個,較先前版本略為減少,發現 imul
被換成 lea
指令
參考 var-x 的 writeup ,可以利用 perf stat <command>
這個命令測量 cpu cycle
perf stat
會遇到權限問題,需要修改 /proc/sys/kernel/perf_event_paranoid
sudo sysctl <parameter>=<value>
快速修改,ref於同個 c 檔案定義好兩個除法函式後,以 main()
呼叫之。編譯出執行檔後測試 perf stat ./div10
:
初始版本的除法耗費 977,489 個 cycle,而不用除法模數的版本只花費 880,667 個,這是很大的進步
注意先前的 div10 指令,預期輸入是不比 19 大的正整數,但現在有效範圍必須是 uint32_t
以內
Modulus without Division, a tutorial 一文針對模數處理,提出了不需除法指令的實作
考慮 modulo 5,已知 ,因此符合以下條件:
可以用這個式子,加上迴圈判斷數值是否還需要做減去的動作。
這個原則可以運用於任一正整數模數,即:
其中 m 為任一正整數,n 為非負整數
但是這麼做的時間成本還是太高,其中一種作法是用 mod 15 推算出 mod 5
因為 mod 15 存在快速用位元運算計算的方式
至於 mod 9,目前想不到較為直接的方式計算
我想到的一種方法為先計算 mod 63,再推算出 mod 9
程式碼回傳 log2 數值
顯然,只須回傳最高位數 (MSB) 是第幾位即可
對於 32 位元的無號整數,則可以透過 GNU extension 的 __builtin_clz()
達成
透過 google 搜尋,找到 bootlin 這個網站,裡面記載了許多核心的程式碼
其中可以找到位於核心目錄的 /include/linux/log2.h
針對 arch 架構,允許透過在 asm/bitops.h
撰寫比使用 fls
更有效率的 log2 實作
這也解釋為什麼許多標頭檔都會加上 #ifndef...endif
這類的巨集
可以發現具有常數 log2 的實作方式
根據 gnu 官網,__bulitin_constant_p()
可用於檢驗表達式在編譯時間是否為常數
參考 編譯器和最佳化原理篇,在編譯器最佳化的策略上,經常可以看到將迴圈內容展開的步驟,稱作 loop unrolling
也有提供非常數的實作:
原來在 linux 核心追求的程式設計,即使是像 log2
這樣基本的函式,不只有正確性的考量,也需要考量硬體配置的差異 (如 32 位元及 64 位元)
根據 EWMA 公式:
對應到程式碼,有以下架構:
internal
對應到 ,
由於平均數涉及到小數點,這裡使用 factor
表示定點數,weight
則對應
於是 ewma_add
對應到上方公式
TODO