執行人: dockyu
專題解說影片
Daniel-0224
假設目標是求
可以視為
多打了 $$
。
yu-hsiennn
這個 2 是? 乘上 1 還是 0? 如果不看後面的推導會不知道這個等式怎麼成立的。
這裡的 2 要乘上 0,我已經把 0 加上去了,我的原意是想讓讀者了解每一個 L 型都可以寫成這個形式
顯然
因為前方寫的是 ,以直覺來說,會有點卡住為什麼會成立(通常數字會遞增呈現),或許可以加一些敘述,如 指的是由 到 的總和。
現在這樣就夠清楚了
第 3 週測驗 5 的程式碼,若將 1 帶入,結果會為 1,但是 ceil_ilog(1)
應為 0。
我只依照 extra 的說明處理了輸入為 0 的情況,經過你的提醒之後才發現題目給的程式碼並不能處理輸入 1 的情況,可以將目前遇到的問題簡化為: 在 時結果要減 1,換句話說就是最後一行的
+1
不要做就可以了,所以如果將+1
替換為+(x != 1)
就只會在 時+0
(比原本的版本少 1),可以在輸入 1 時正確回傳 0,完整程式碼已經更新在 測驗五
marsh-fish
建議在探討 SWAR 的時間可以把相關的函式獨立計算時間,比較好看出時間差異。
ChenFuhuangKye
等同於 約等於 ,之所以使用 13 跟 128 是因為 除以 128 可以透過位移做到,乘以 13 也可以透過 bitwise 做到,且這個數字在 時的誤差在小數點後一位之下,當然也有其他數字更接近 不過題目已經確定被除數最大為 19,所以 已經足夠
被除數最大為 19 有錯字要更正,可以列出 0 ~ 19 的誤差嗎?
錯字已更正
數字 的誤差會是 ,當 時誤差為 ,若 則誤差會比 來的小
david965154
此處用途為何,為何只在前方做判斷,能保證記憶體位置的後方無需做此種處理嗎?
重作第 3 週測驗題、第 4 週測驗題, 第 7 週測驗題和延伸問題,探究 bitops 在 Linux 核心的應用。
包含延伸問題
版本一對 取 , 參考以下的表格可以發現其實 就是在取 的 most significant bit (MSB)
十六進位 | =msb | 1 << msb |
|
---|---|---|---|
1 | 0001 |
0 | 0001 |
2 | 0010 |
1 | 0010 |
3 | 0011 |
1 | 0010 |
4 | 0100 |
2 | 0100 |
6 | 0110 |
2 | 0100 |
8 | 1000 |
3 | 1000 |
從上面的表格可以看出再開平方根時完全不需要考慮 1 << msb
以上的位元,所以只要遍歷逐一走訪比 1 << msb
小的所有可能,版本一的方法是每次都將 (1 << msb) >> 1
加上去,直到變為0
因為 的目的是得到 msb,版本一使用 math library 裡的 log function,但是也可以透過 bitwise 做到
你所不知道的 C 語言:數值系統中也有提到,當我們計算 (以 2 為底的對數) 時, 其實只要算高位有幾個 0's bits. 再用 31 減掉即可
所以也可以改寫成以上形式
假設目標是求 ,, 可以視為
$$
是上面的矩陣的每一項相加如果將矩陣拆如下可以找到規律
顯然
目標是求 , 但是 會由前一個得到,所以要先知道 -> -> -> -> ,而 也是由 得到的,所以可以初始化 因為從 的定義裡 不會是任何數值的相加
每一次都判斷 是否成立,也就是版本一&版本二的每一次迴圈做的事
但是每一次都計算 太麻煩,我們只需要判斷 有沒有大於 0 就可以了
每一輪都只需要 計算 & 並判斷 就可以了,需要紀錄 做為下一輪的
可以用 , 用 bitwise 快速求出 ,
初始化 n
即為所求
這裡的 m 就是 ,每次迭代都會乘上 也就是右移 2,z 則是 每次迭代都會乘上 也就是右移 1
commit: 84ac45f
延伸問題:
ffs
/ fls
取代 __builtin_clz
,使程式不依賴 GNU extension,且提供分支和無分支 (branchless) 的實作。__builtin_clz
clz 等於 31 - fls
,所以 31 - __builtin_clz(x)
等於 31 - (31 - fls)
等於 fls
fls 程式碼
commit: 40b7d29
我發現 fls 每次 x 要位移以及 r 要減掉的數值都一樣,所以可以用同一個變數來表示,而這個數值都是 2 的冪,所以都可以用1位移得到
注意書寫規範:
原本的版本為 0 才需要操作,所以如果將如果為 0 時可以得到 1 非 0 時得到 0 ,就可以用這個結果位移使的變數的值只有 0 或是目標值(要位移、要減掉的值)
commit: 12f6334
提供數學證明。
參見 Linux 核心原始程式碼的整數除法 和 從 √2 的存在談開平方根的快速運算
等同於 約等於 ,之所以使用 13 跟 128 是因為 除以 128 可以透過位移做到,乘以 13 也可以透過 bitwise 做到,且這個數字在 時的誤差在小數點後一位之下,當然也有其他數字更接近 不過題目已經確定被除數對大為 19,所以 已經足夠
注意書寫規範:
log2 就是在算二進位表示下最左邊的1代表的數值是2幾次方,例如 ,最大的是所以
版本一
這個版本不斷右移直到 i 為0,此時位移的次數是最左邊的1的位置,假設最左邊的 1 是第 x 位,代表的數值是 ,所以只要紀錄位移次數再減1就會是答案
注意書寫規範:
版本二
這個版本用二分搜尋法每次只判斷一半的長度的最大值,如果是就代表最左邊的1在右邊那一半的左邊,所以直接加上一半的長度
因為每次一半都只需要執行一遍,所以不需要用到迴圈,可以改寫為
最下面並不是一半,所以還是必須一個位元一個位元檢查,但是前三次已經節省了非常多次檢查,這裡最多只需要檢查8次
版本三
計算最高的 set bit 其實就是算左邊有幾個 0,可以使用 clz 函數來得到,v|1
是為了必面傳入 0 讓 clz 回傳未定義的結果
延伸問題:
2. 在 Linux 核心原始程式碼找出 log2 的相關程式碼 (或類似的形式),並解說應用案例
drivers/net/ethernet/amd/pds_core/core.c
看這個檔名應該是和網路相關的功能會用到 ilog ,但是我看不懂他的用途
一開始我覺得程式碼和公式對不上,參考 vax-r 的筆記後 才知道這是定點數運算,證據是 ewma_read 時應該要回傳目前的,但是實際回傳的卻是 avg->internal >> avg->factor
所以 avg-factor
應該就是定點數的 scaling factor 了
如果把 暫時替換成 ,不為 0時可以看作
,可已看出 是
延伸問題:
2. 在 Linux 核心原始程式碼找出 EWMA 的相關程式碼 (或類似的形式),並解說應用案例,至少該涵蓋無線網路裝置驅動程式。
drivers/net/wireless/ralink/rt2x00/rt2x00link.c
rt2x00link_get_avg_rssi
函式傳入一個 struct ewma_rssi *ewma
, 他的功能是取得無線網路的 RSSI,並且函式中有一行 avg = ewma_rssi_read(ewma);
看起來在取得平均值,但是奇怪的是我在 linux 的程式碼裡找不到這個函數的定義
因為要對 取 ceil,可以先觀察 持續增加時什麼時候 會增加,當 x 剛好是 2 的冪時會是這個結果的最大的 x ,例如當 時 ,當 時 觀察 8 (1000) 和 9 (1001) 的二進位,在 x--;
時8的對高位 set bit 會變成 3 而 9 不會變(還是 4),在 shift = (x > 0x3) << 1;
後此時 r 和 shift 都是被位移過後的數值所以第 1 個位元一定是0,x >>= shift;
因為 shift 一定是 2 如果 只有 3 位元那位移後只有 1 位元最大就是 1,判斷 x 是否大於1就可以用來當作要不要+1的標準
延伸問題:
2. 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless
應該是無解的所以我希望 可以輸出 -1
為何選 -1
?在工程上有無額外風險?
因為 0 以及正數是 的可能輸出值,所以無解的問題我認為應該不能是 0 或正數,那就只能輸出負數了
Branchless code that maps zero, negative, and positive to 0, 1, 2 有一則回應
可以把正數對應到 1,0 對應到 0,-1 對應到 -1
在這一題中只會有正數以及 0,分別討論這兩種情況
正數 | 0 | |
---|---|---|
c | 0x00000001 | 0x00000000 |
c-1 | 0x00000000 | 0xFFFFFFFF |
如果把最後的結果和 c-1 做 or 運算,正數不會變,而0則會變成-1
上面的程式碼在 時會回傳 1,但是正確答案是 0,所以 時要減 1,根據程式碼也可以理解為 時不要做 +1 這件事,如果將 +1
改為 +(x!=1)
就只會在 時變成 +0
包含延伸問題
這個方法可以只看其中一個 set bit 來解釋,如果x的第5個 bit 是1,那在 (x>>1) 的 會是1,在 (x>>2) 的 會是1,在 (x>>3) 的 會是1,在 (x>>4) 的 會是1
因為 所以 由此可知這個方法每個 set bit 會帶給結果 1,所以有幾個 set bit 結果就會是這個值
如果對每個 nibble 這麼做最後每個 nibble 中的 set bit 數量就會存在這個 nibble 裡,下面的程式碼就是在將每一個 nibble 相加
因為兩個迴圈會讓一個 pair 被算到兩次,所以結果要除以2,等同右移 1 bit,這個方法在 LeetCode 477. Total Hamming Distance Time Limit Exceeded
可以將第二個迴圈的起始設為 i+1 就不會有重複計算的問題,所以也不用除以2,可以通過 LeetCode 的測試
所有 Number 中從第 0 個 bit 開始計算全部該 bit 的 Hamming Distance ,在全部累加起來就是 Total Hamming Distance
LeetCode submit
我本來想直接寫32個判斷式,後來同學提醒我可以位移後和1做 and 運算,就可以判斷出特定位元是不是1
經知道 其實就是奇數與偶數 bit 的1的數量的差
並且可以推導出下面的公式
第3行其實就等於奇偶的差+39
,因為只有 32 位元,所以奇偶的差的範圍是 -16 ~ 16
,第一行的 n 的範圍是 23 ~ 55
,所以要再做一次但是因為最大才55(只會用到最前面6個位元)所以可以用 0x2A 當作 m ,做完第二行後 n 的範圍是 -3 ~ 3
(其實不會有3這個結果,因為只有在 時才會是 3,但是這不在第一行的結果範圍裡),如果是負的後面 ((n >> 31) & 3)
會是3,把 n 補回正的(如下表)
((n >> 31) & 3) |
n + ((n >> 31) & 3) |
||
---|---|---|---|
-3 | 0xFFFFFFFD | 3 | 0 |
-2 | 0xFFFFFFFE | 3 | 1 |
-1 | 0xFFFFFFFF | 3 | 2 |
0 | 0x00000000 | 0 | 0 |
1 | 0x00000001 | 0 | 1 |
2 | 0x00000002 | 0 | 2 |
第3行明顯跟上面推導的公式不一樣少了 -16
,所以 n 的範圍是 0 ~ 32
,而 實際上 的結果是-16,對照下表,因為範圍是 0 ~ 32
而且我們已經知道每一個數的答案,那麼直接用一個 lookup table 比較快
正確答案 | ||
---|---|---|
0 | -16 | 2 |
1 | -15 | 0 |
2 | -14 | 1 |
3 | -13 | 2 |
4 | -12 | 0 |
31 | 15 | 0 |
32 | 16 | 1 |
延伸問題:
Hackers Delight 給出兩個版本
所以經過三次操作之後的 只是 的範圍被縮小到 0x4A
以下,已經可以直接用 table 列出所有解
只會是 0~6
0 | 0 | 0 |
1 | 1.1 | 1 |
2 | 2.2 | 2 |
3 | 3.4 | 3 |
4 | 4.5 | 4 |
5 | 5.7 | 5 |
6 | 6.8 | 6 |
由上表可知在所有情況下 ,而 可以透過 得到
所以第一行 n = (0x24924924*n + (n >> 1) + (n >> 4)) >> 29
就是在做 ,因為一開始 0x24924924*n
等同 如果將 拆成 ,以上的部分會溢出,相當於被捨棄掉了所以 的範圍只會在 可以表示的範圍裡也就是 0~7
(int)(n - 7) >> 31 |
|||
---|---|---|---|
0~6 | 負數 | 0xFFFFFFFF |
因為有號數負數右移會補1 |
7 | 0 | 0x00000000 |
所以當 時和 0xFFFFFFFF
做 &
運算結果不變,當 時和 0x0
做 &
運算結果為 0,因為
理解這個程式碼之後我突然冒出一個問題,這個函數適用於所有數字嗎?因為他是靠捨棄 當作取 floor ,但是如果 很大,導致 那不就會多捨棄了1嗎?這樣做出來的 remainder 應該會少1?
本手法有輸入範圍的限制。
包含延伸問題
SIMD within a register (SWAR) 是軟體最佳化技巧之一,讓程式可以在特定的資料格式上平行處理
一次判斷一個 unsigned long,所以 mask 裡要放滿要找的字 d ,因為32位元和64位元的系統的 unsigned long 長度不同,所以需要在 mask 不夠長時將它複製成兩倍長
每次都判斷一個 LBLOCKSIZE 有沒有 d
判斷方式是,如果有 d ,那這個 unsigned char 的空間和 ~d 應該要完全顛倒,and 運算出來為0
接著再在這個 LBLOCKSIZE 裡找到 d 即可
DETECT_NULL 的判斷方式
拆開成 8 bit 來看, ,如果 非0 那麼有兩種可能
延伸問題:
2. 比較 Linux 核心原本 (與平台無關) 的實作和 memchr_opt,設計實驗來觀察隨著字串長度和特定 pattern 變化的效能影響
這是 linux memchr 的程式碼,一次只判斷一個 char
當可以找到 d 時,執行時間只和 d 的位置有關,當找不到 d 時執行時間應該和字串長度有關,所以我的實驗會分成兩個情況討論
固定 string 的長度為 100005 ,紀錄第一次出現的 d 的位置增長會花費的時間的變化
觀察不同長度的字串在不包含 d 的情況下的執行時間
延伸問題:
研讀 2022 年學生報告,在 Linux 核心原始程式碼找出 x86_64 或 arm64 對應的最佳化實作,跟上述程式碼比較,並嘗試舉出其中的策略和分析
下一步:貢獻程式碼到 Linux 核心
Phoronix 報導
We use word-wide comparing so that 8 characters can be compared at the same time on CPU.
可以同時比對8個 char ,速對會快差不多4倍,不過我做的實驗沒有快到這麼多
[PATCH 0/2] x86: Optimize memchr() for x86-64 裡有對於 memchr 的最佳化的程式碼,可以直接跳到 arch/x86/lib/string_64.c ,(但是我在現在的 linux 裡已經找不到這個檔案了只有 arch/x86/lib/string_32.c)
Linux 核心專題: 重做 lab0
Linux 核心專題: 井字遊戲改進
Linux 核心專題: 浮點數運算案例探討
Linux 核心專題: 高效網頁伺服器
Linux 核心專題: 重作第四次作業