contributed by < KJay221
>
目前解不了的問題(文中黃色區塊標記)
1
考慮以下對二個無號整數取平均值的程式碼:
A
解決 overflow 的問題:
B
改寫為以下等價的實作:
C
再次改寫:
D
EXP1 = a & b & 1
EXP2 = a & b
EXP3 = a ^ b
B段程式:
上述為取平均之公式
C段程式:因為當兩數都為奇數,且都右移一位時,總共會少二,所以在取平均時就會少一,所以就要先測試兩數是否為奇數(a & 1 和 b & 1),若是則a & b & 1 = 1
,再加回去剛好就是答案
例:3 和 7 皆為奇數所以
D段程式:參考你所不知道的 C 語言:數值系統篇
將加法器的邏輯轉換成程式,x & y
為進位,x ^ y
為低位數,又取平均為
(x + y) >> 1
= (((x & y) << 1) + (x ^ y)) >> 1
= (x & y) + ((x ^ y) >> 1)
執行: gcc -O3 -S q1.c
可得assembly code
C段程式:
D段程式碼省了3行指令,而且只需要用到一個暫存器
首先探討 Exponentially weighted moving average 是什麼,根據 wiki 上的解釋, EWMA 是以指數式遞減加權的移動平均,越舊的資料加權越小並以指數程度遞減,加權程度以常數 α 決定, α 介於 0 到 1,計算公式如下:
然後將遞迴展開
因為 1 - α < 0
,可以看出越舊的資料權重越小
接著再來看 linux 程式碼
可以看出整段程式都是用 macro 撰寫,另外一共有三個 argument 功用分別為
name
:主要影響 struct 及 function 的名子,像是當 name 為 price 時 ewma_##name
會變成 ewma_price
,##
的用法參考你所不知道的 C 語言:前置處理器應用篇_precision
:控制精度的一個參數,像是在 ewma_##name##_read
的return 值 e->internal >> (_precision)
利用 right shift _precision
個 bits 來控制回傳值的精度_weight_rcp
:加權程度常數,也就是上述數學式中的 α 取倒數,另外因為效能因素,此參數只能用 2 的冪,在程式中則是用 BUILD_BUG_ON_NOT_POWER_OF_2
來檢查參數是否為 2 的冪接著一段一段看
此段程式依照 name 的內容來宣告 struct
此段程式用於初始化上述 struct 結構變數,只要傳入指標就能進行初始化,另外此處用到許多 BUILD_BUG_ON()
,參照 linux/include/linux/build_bug.h 第 41~50 行中的註解 break compile if a condition is true
,在這裡檢查了 _precision
和 _weight_rcp
兩個參數是否為常數,並確保 _weight_rcp
為 2 的冪,若不是則會在編譯時出錯,最後再將 struct 中的 internal 初始化為 0
此段則是輸入 struct 指標後回傳其 internal 值,首先檢查各項條件,若通過則利用 >> (_precision)
來控制回傳精度並回傳 internal 值
最後這段的功能則是加入一個新的數 val 並將 EWMA 更新,其中 READ_ONCE()
和 WRITE_ONCE()
都是要確保 Cache coherence
,前三行是將算式所需要的變數處理好,接著再檢查條件,最後在 WRITE_ONCE()
將值更新
(((internal << weight_rcp) - internal) + (val << precision)) >> weight_rcp
= ((internal << weight_rcp) - internal) >> weight_rcp + (val << precision) >> weight_rcp
(val << precision) >> weight_rcp
對照 ((internal << weight_rcp) - internal) >> weight_rcp
= internal - (internal >> weight_rcp)
對照 上述程式中
為什麼要用 30???
先將 linux 核心整個 clone 下來,然後用 grep -nr 'ewma_' .
找有用到的地方
或是利用 ag
ag 'ewma_'
ag -G wireless 'ewma_'
無線網路因為訊號強度會不停改變,所以可以用 ewma 來計算評估訊號強度,將過去一段時間訊號強度乘上不同的權值來代表訊號強度,另外在電信領域 RSSI 是一種量測接收訊號強度的指標,可以用 ewma 計算後的結果當作此指標的依據,以下在是 wifi chip driver 運用 ewma 和 rssi 的例子
linux/drivers/net/wireless/mediatek/mt7601u/phy.c
以上程式依照 ewma_rssi 的大小修改 val 的值
再看到前面宣告 u8 val = mt7601u_agc_default(dev);
可以知道 val 是一個與 agc 有關的值,參考 Automatic gain control 和 AGC的作用(自动增益控制),簡單來說 agc 是針對不同信號強度使用不同程度的放大,達到最終信號輸出維持一定範圍內大小,所以上面程式就依照 rssi 大小來調整 agc 的值
在 Start/stop collecting beacon data 時將 ewma 值初始化
linux/drivers/net/wireless/mediatek/mt7601u/mac.c
某些情況下更新 ewma_rssi 值,目前能力不足無法解讀前後程式意思
mt76_mac_process_rx
函式的功用?mt76_mac_process_rx
函式改 ewma_rssi 的值DRM 是 linux 的圖形顯示框架,相關內容參考
drm_self_refresh_helper.c 中大量用到 ewma,主要是用於 psr_time
psr 技術主要是用於省電,一般來說螢幕需要靠 GPU 不斷運算來刷新影像,所以像是在玩 3D 遊戲時電量消耗較大,反之在瀏覽網頁或閱讀電子書時不需要一直刷新螢幕,這時候就要靠 psr 技術將之前儲存的數據顯示於螢幕上,這樣就能減少 GPU 運算,從而達到省電目的,以上參考Tcon的省电功能PSR(panel self refresh)简介
2
Branchless Max
EXP4 = a ^ b
EXP5 = a < b
a > b
則 -(a < b)
的值為0a ^ ((a ^ b) & -(a < b))
= a ^ 0
= a
a < b
則 -(a < b)
的值為-1 (0xffffffff)a ^ ((a ^ b) & -(a < b))
= a ^ (a ^ b)
= a ^ a ^ b
= 0 ^ b
= b
-(a < b)
這個運算中, a 與 b 的正負值並不會影響結果,結果只與大小有關,又觀察上面程式結果a > b
=> a ^ ((a ^ b) & 0)
= a ^ 0
= a
a < b
=> a ^ ((a ^ b) & -1)
= a ^ (a ^ b)
= a ^ a ^ b
= 0 ^ b
= b
if (i & lsbit) i -= size;
改寫成 i -= size & -(i & lsbit);
當 (i & lsbit)
為 true 的時候 i -= size & -(i & lsbit)
= i -= size & -1
= i -= size
當 (i & lsbit)
為 false 的時候 i -= size & -(i & lsbit)
= i -= size & 0
= i -= 0
先用 git log --oneline --grep branchless
和 git log --oneline --grep branch-free
找到符合的 commit hash
再用 git show hash
來查看紀錄,或是在 github 上查看比較清楚
這邊探討 Replace int2float() with an optimized version. 案例
此 function 的功能是將 unsigned integer 轉換成 32-bit IEEE floating point representation
原版:
需要用 for 迴圈來確認 exponent 大小,並同時修改 fraction 值,因為 fraction 部份剛好 23 bits ,所以用 fraction & 0x800000
來確認 fraction 部份是否有超出範圍
改進版:
__fls
的功能是找到 most-significant set bit ,這裡用 __fls
就可以省去用 for 迴圈確認 exponent 的部份,只要 127 + __fls(x)
,然後再 shift left 23 就符合浮點數 exponent 的表達
3
64 位元 GCD 求值函式
改寫為以下等價實作:
COND = v
RET = u << shift
處理 or or 的情況
將二的冪公因數先除掉以減少後續計算量,並將二的冪公因數資訊存到 shift 中
處理 u 為偶數,v 為奇數的情況,2必定不為公因數
先處理 v 為偶數,u 為奇數的情況,接下來就是做輾轉相除法,若 v 為零則輾轉相除法結束,若為非零則繼續做,最後將結果 u 左移 shift 次用以乘回之前多除的二,然後再將結果 return 即為所求
__builtin_ctz(x)
回傳為 x 最低位元有連續幾位是0,像是 10100000
會回傳 5
shift
中,接著用 right shift 將 u
除成奇數,對應原版的 codev >>= __builtin_ctz(v);
來改寫原本除法的部份先測試三個 function 的執行時間,測試程式連結,測試方法為產生 100000 組亂數,測試三個 function 執行完全部資料所需的時間,執行結果如下
多執行幾次也得到差不多的結果,我原本以為速度快到慢的排序應該是(3)(2)(1),沒想到卻是(1)(3)(2),接著用 perf 分析,將每個函式分開執行測試
(1) 花最多時間在除法上。取餘數在組合語言中也是用除法指令來做,雖然有用到除法,但在其它運算上省下的時間可以彌補除法所花費的時間
(2) 花最多時間在 mov
上,可能是有太多賦值運算的關係
(3) 花最多時間在 mov
上,另外 tzcnt
為 Count the Number of Trailing Zero Bits
但如果開啟編譯器最佳化 gcc -O3 -o q3.out q3.c
則會得到下面結果
經過最佳化後,版本(3)的速度會是最快的,接著用 perf 分析
(1) 一樣是花最多時間在除法上
(2) 花在 jump
和 mov
的時間較多,根本問題還是 branch 太多及太多賦值運算
(3) 可以看出花最多時間在 sub
和 tzcnt
和 shr
但這三項都是必要運算
主要有兩種實做,一種是有 __ffs
,一種是沒有的,__ffs
的功能與 __builtin_ctz
是一樣的,首先解釋有 __ffs
的版本:
a | b
舉例 a = 11010000 , b = 10000100
做 |
的話必定能將兩者最低位 1 保留, a | b = xxxxx100
r & -r
這個操做可以將 r
的最低位 1 保留其它 bits 設為 0r = 11011000
-r = 00101000
可以先不看最低位 1 以後的 bit 會比較好理解 r = xxxx1000
-r = xxxx0111 + 1 = xxxx1000
x
的部份會剛好都相反, &
完後都會變成 0沒有 __ffs
的版本:
a -= b;
後程式碼參考 Nomad1230 解釋, a -= b;
時 a
與 b
必同為奇數或偶數,所以相減結果必為偶數,用 a >>= 1;
可將相減所產生的 2 除掉,而 if (a & r)
則是防止當除過頭時,則將 b
加回來,最後再執行 a >>= 1;
,上述這些步驟的目的是為了減少迴圈數量,實際測試程式,跑 100000 組亂數測試有沒有加上述程式對時間的影響,測試時用 -O3
最佳化編譯結果如下
之前看到 vacantron 的測驗三有討論 __ffs
的功能,後來去看原始碼發現 ffs
和 __ffs
是不一樣的
__builtin_ffs(x)
是一樣的,都是回傳從低位 bit 數來,第一個 bit 1 是在第幾個 bit ,其值與 __builtin_ctz(x) + 1
相同,相關程式碼在 linux/include/asm-generic/bitops/ffs.h__builtin_ctz(x)
是一樣的,相關程式碼在 linux/include/asm-generic/bitops/__ffs.hlinux/drivers/gpu/drm/radeon/radeon_display.c
上面程式中 gcd 是用來約分分數,在 gpu driver 裡出現
4
在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
用 __builtin_ctzll
改寫為下列程式碼
EXP6 = -bitset & bitset
主要的功能是將 bitmap 中 1 的總數記在 pos 中,並將每個 1 的位子分別記在 out array 中,其餘解釋打在程式的 comment 裡,實際跑一次會比較清楚到底這個 function 在做什麼
目前找不到關鍵字去核心尋找到相關程式
實驗程式碼,測試不同資料量及不同 bitmap density 兩函式所需要的執行時間,實驗結果如下
圖片繪製,先將 code clone 下來然後執行:
從上面的圖可發現改進版的整體效能比原版的還高,但有兩個特別的點
原版函式當 bitmap density 靠近中間值時所需的時間也越多,反而 bitmap density 越高所需的時間也越少,初步推測是因為 branch prediction 的問題,後來實際去用 perf 去測試相同資料集跑 100 次得到下列結果
bitmap density = 64
bitmap density = 32
可以看出在 bitmap density = 32 時 branch-miss 是較高的,推測是因為每個 bit 是 set bit 的機率是 50% 所以 CPU 很難猜下個 bit 是不是 set bit , branch-miss 的比例自然也高,但在 bitmap density = 64 的狀況下,每個 bit 都是 set bit,所以 CPU 自然會猜下一個 bit 為 set bit , 所以 branch-miss 的比例就會很低,而 branch-miss 越多的情況自然會降低效能的表現
改進版的隨 bitmap density 上升所需的時間也跟著成長,觀察 code 可發現當 density 越高每次呼叫函式所需要跑的迴圈次數越多,且 __builtin_ctzll
的 latency 較高,每次迴圈所需的時間較長,所以當 density 越高與原版的時間差距也越小,反之 bitmap density 越低,使用 __builtin_ctzll
的效益越高
在上面的測試中可發現 bitmap density 越低,使用 __builtin_ctzll
的效益越高,當 bitmap density 越高用 __builtin_ctzll
反而優勢就消失了,所以可以從高 bitmap density 的情況下去改進,參考 qwe661234 的想法和程式,改進 naive 版本一次迴圈只測一個 bit 的缺點,變成一次迴圈測兩個 bits ,並用 bitwise 來改寫一些算式
另外也有寫一次迴圈測四個 bits 的程式
將兩者與 __builtin_ctzll
版本放在一起比較,測試跑 bitmapsize = 10000
的情況,結果如下
可以發現一次測四個 bits 所需要的時間比一次測兩個 bits 所需的時間還要少,且兩者在 bitmap density 較高時都比 __builtin_ctzll
版本好,而 __builtin_ctzll
版本在 bitmap density = 0 ~ 40 左右有絕對的優勢
首先這個檔案在做的事就如註解所講的
Cpumasks provide a bitmap suitable for representing the set of CPU's in a system, one bit position per CPU number.
在程式的開頭先定義結構
DECLARE_BITMAP
則自於 linux/include/linux/types.h
name 為 bitmap 的名子,而 bits 則是需要的 bit 數量。在這裡 bits 參數為 NR_CPUS
,代表系統所支持最大 CPU 數
定義完 bitmap 結構後下面則有許多應用例如
在這裡有四種 CPU 狀態的 bitmap
possible
: 標示可移至內核的 CPU
online
: 標示已經移至內核的 CPU
present
: 標示已經初始化後可被調用的 CPU
active
: 標示哪些 CPU 可能已經被某些任務佔據
上面是 set_cpu_possible 的 function ,用於更改 __cpu_possible_mask
中的 bit ,其中 bool possible
決定將 bitmap 特定 bit set to 1
or set to 0
,至於是哪個 bit 要 set 則是由 unsigned int cpu
來決定,這裡又會用到另外兩個 function
在這裡 set_bit
和 clear_bit
的第一個參數都為 cpumask_check(cpu)
, cpu 代表是第幾個 bit 要被 set , cpumask_check() 則是確認是否有超出範圍 , 而第二個參數為 cpumask_bits(dstp)
, dstp 為 bitmap struct 的位置 cpumask_bits() 則是取 struct 中 bitmap 的值
另外還有許多其它應用像是
cpumask_first
對應 find_first_bit
cpumask_next
對應 find_next_bit
cpumask_next_zero
對應 find_next_zero_bit
等就不一一列舉介紹了
參考:
Linux进程管理之CPU启动相关的四大状态
Linux 内核 | CPU 状态管理
CPU masks
5
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作:
PPP
= pos--
MMM
= list_add
EEE
= &heads[remainder % size]
例如,判斷負號只要寫作
bool isNegative = numerator < 0 ^ denominator < 0;
搭配研讀 The simple math behind decimal-binary conversion algorithms
denominator != 0
division
必定是大於零,所以改寫成以下
key % size;
就可以改為 key & (1 << k - 1)
,這樣就可避免使用 % 以提高效率目前用關鍵字 fraction 去搜尋只找到下列相關檔案,但裡面 fraction 的意思和題目所要探討的不同且與 bitwise 操作也無關,目前想不到如何尋找題目所要求探討的程式碼部份
linux/mm/kasan/quarantine.c
linux/mm/slub.c
linux/mm/page_alloc.c
linux/mm/vmscan.c
6
alignof 是 GNU extension,以下是其可能的實作方式:
M
= _h
X
= 0
這裡最後用 (char *)
轉型是因為一個 char 的空間為一個 byte,以下將 char 改成 int 測試 t = uint64_t
,結果會是 2
另外因為 data alignment
,所以 compiler 會將 struct 的大小對齊 struct 裡最大的元素,所以 char c
後會 padding some bytes,這樣相減結果就會知道 alignment requirement
linux/net/netfilter/ipset/ip_set_bitmap_port.c
在上面這兩個例子中 __alignof__
都和 __aligned
並用,並且在 linux 核心中許多地方也都有他們兩個並用的情形,這樣的用法是為了對某些變數做特定大小的 alignment ,像是第一個例子就是先用 __alignof__(struct sched_class)
取得要對齊的大小,接著再用 __aligned()
來對齊 const struct sched_class name##_sched_class
ALIGN
, ALIGN_DOWN
, ALIGN_UP
等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集若上述程式 x = 6
a = 4
會得到結果 ALIGN: 8
ALIGN_DOWN: 4
,可以發現 ALIGN
是向上對齊, ALIGN_UP
可能因為功能與 ALIGN
相同所以才沒有,而不管是 ALIGN
或是 ALIGN_DOWN
都有用到 __ALIGN_KERNEL
linux/include/uapi/linux/const.h
舉個例子比較好理解程式的運作, x = 6
a = 4
, __ALIGN_KERNEL(6, 4) = __ALIGN_KERNEL_MASK(6, 3)
,然後兩者相加會變成 9 ,而 9 的二進制為 0x1001
,然後再 0x1001 & ~(0x0011) = 0x1000
,可以發現若沒有剛好對齊, x + mask
就會變成下個 4 的倍數再加某數,像是 6 + 3 = 8 + 1
7 + 3 = 8 + 2
,然後再用 mask 遮掉後面的數讓它剛好為 4 的倍數就完成了向上對齊,而向下對齊就是先減,讓它 4 的倍數部份減少再向上對齊而已
在許多地方都有用到,像是記憶體管理 linux/mm/page_alloc.c
上面用 ALIGN
來使 page frame number(pfn)
對齊 pageblock_nr_pages
,程式主要功能是確認某一個區塊的記憶體是連續的
這裡 ALIGN_DOWN
也是用來使 page frame number(pfn)
對齊 pageblock_nr_pages
7
直覺的實作程式碼如下: (naive.c)
觀察 printf 的(格式)字串,可分類為以下三種:
考慮下方程式碼:
我們若能精準從給定輸入 i 的規律去控制 start 及 length,即可符合 FizzBuzz 題意:
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
KK1
= div3
KK2
= div5
KK3
= div3 << 2
naive.c
和 bitwise.c
效能落差,避免 stream I/O 帶來的影響,可將 printf
更換為 sprintf
首先 is_divisible
的原理用例子比較好解釋
12 可被 3 整除:
若是整除, (0xff… + 3) 的部份會 overflow 後變成很小的數,回傳值也會變成是 1
20 不可被 3 整除:
若不是整除必可拆成三加上某數,多出的部份乘起來必定比右式大,所以回傳值也會變成是 0
接著 div3
div5
有四種組合,對應到 length
和 start
(div5, div3) | (0,0) | (0,1) | (1,0) | (1,1) |
---|---|---|---|---|
length | 2 | 4 | 4 | 8 |
strat | 8 | 0 | 4 | 0 |
再看以下,發現剛好可以對應到正確的輸出
length
只要多整除一個數,輸出便會多一倍,所以用 left shift 剛好,而 start 只要遇到可以整除 3 便會變成 0 ,所以必須 8 right shift 4 ,而若是只有整除 5 只需要 8 right shift 1 就好
測試跑 1000000 筆資料時間。
naive
:0.037916832 s
bitwise
:0.042828712 s
結果發現 bitwise 版本竟然比較慢,試了幾次都是相同的結果,所以想說看什麼指令花比較多時間,於是就先將 sprintf
先改成 count++
來測試,不然若是用 sprintf
則會佔用太多時間而無法觀測其它指令時間差別
naive
發現花最多時間在 shl
和 imul
,然而卻沒有任何除法指令,初步推估是因為編譯器優化掉除法指令了,這可能是造成原版竟然比 bitwise 版還要快的原因
bitwise
而 bitwise 版則是花最多時間在 call function
及 mov
上
bitwise.c
程式碼,試圖運用更少的指令來實作出 branchless,參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware看了上述資料後還是想不到如何改進,後來偶然間看到另一種作法,參考奇葩编程之FizzBuzz,作者的想法主要是利用模同餘的性質配上 bitwise 操作,將所有數字映射到 0~7 之間
簡單來講分成兩個部份,第一個部份是將所有數字映射到 0~15 ,作法則是利用模同餘的性質, ,所以只要不停的做模同餘變換,每次都用 16 的倍數下去切再做加減,最後就會映射到 0~15 之間,接著就是如何將 0~15 映射到 0~7 ,可以發現 15➜0 12➜3 9➜6 10➜5 ,上述的整除性質都一樣又 15 - k 和 15 ^ k 的結果是一樣的,然後 8~15 最高位都是 1 所以利用最高位位移加上 xor 即可做出映射變換
一樣是測試跑 1000000 筆資料時間,最近發現可以用 -r 參數來取多次實驗平均結果
時間:
naive
: 0.0339814 ± 0.0000960 seconds time elapsed ( ± 0.28% )
bitwise
: 0.040525 ± 0.000393 seconds time elapsed ( ± 0.97% )
improve
: 0.0364116 ± 0.0000975 seconds time elapsed ( ± 0.27% )
instructions:
naive
: 632,119,366
bitwise
: 689,331,934
improve
: 657,274,531
可以看的出 improve 版不管是在時間上或者是 instruction 數上都比 bitwise 版還要再更好
上述文章內許多組語看不懂,然後有想說試試看用平行程式下去優化,但文章內又提到
Alex did attempt to build a multi-threaded version but was unable to find any performance improvement over the single-threaded version. The reason stated was that the cost of communication between CPU cores is sufficiently high enough that any gains from parallel computation are ultimately negated.
所以目前不太知道下一步如何做
kernel/time/
目錄的標頭檔和程式碼)看不太懂程式碼,不確定快速除法的實做在哪裡