contributed by < st10740 >
這段程式碼的目的是計算以 2 為底的對數,其做法是找到該數字的二進位表示中,最高位元為 1 的索引值,而該索引值即為以 2 為底的對數值。
然而,若是透過線性搜尋 (Linear search) 的方式會找太慢,因此以下方法採取二分搜尋法 (Binary search) 的方式進行搜索。首先會先利用 while (i >= ...)
確認左半邊的位元是否存在 1,若存在則搜尋左半邊,並將 result
加上一半的位元數,若左半邊不存在為 1 的位元,則搜尋右半邊,並不斷縮小範圍直到找到為止。
而另外一個作法的概念也相同,只是利用了 GCC 提供的內置函式 __builtin_clz
,其作用是計算無號整數 v
的二進位表示式中,從最高有效位開始的連續零位的数量。因此透過將 31 減去該值就可以得到最高位元為 1 的索引值。
在 tools/include/linux/log2.h 中定義了巨集 ilog2
。
這個方法會先利用 __builtin_constant_p
判斷 n
在編譯時期是否為常數,若是則會在編譯時期完成如下面 (n) & (1ULL << 63) ? 63
的運算來求得最高位為 1 的索引值,因此 ilog2
的運算結果可以在編譯時期就確定;但若不是常數則需要等到執行時期再進行運算,並且會根據 n
的大小決定呼叫 __ilog2_u32
或是 __ilog2_u64
。因此這個作法可以節省輸入為常數時的執行時間。
我進行了簡單的測試確認以上巨集處理常數的部份是否真的會在編譯時期取得運算結果,因此我拿取了巨集中處理常數的程式碼建立了一個新的巨集,並在 main
中使用該巨集,將常數 1022
作為參數輸入,並將結果印出來。
接著我將程式碼進行編譯,取得其編譯後的組合語言,可以觀察到第 10 行的 9
即為 ilog2(1022)
的結果,在這邊被當作常數來使用。
Exponentially Weighted Moving Average (EWMA; 指數加權移動平均) 是一種取平均的作法,在訊號處理中可以作為濾波器使用,其公式如下:
可以透過 決定新資料與歷史資料的權重,介於 0 到 1 之間, 值越大代表歷史資料的權重減少地越快。
在實作方面,首先宣告一個結構體 ewma
,internal
用來儲存當前算出來的平均值;weight
用來存新資料與歷史資料的的權重, 等同於上面公式中的 ;factor
的目的是保留平均數的小數部份,其值存的是欲保留的小數點位數,因為 internal
的型別為 unsigned long,進行平均值運算時可能會丟失小數部份,因此透過 factor
來避免誤差。
ewma
的初始化確保傳入的 weight
和 factor
都是 2 的冪,原因是為了提升效能,會使用位元運算子進行乘除運算,接著分別取得 weight
和 factor
欲左移或右移的位元數,並將 interval
初始化為 0。
ewma_add
用以計算加入新資料後的整體平均值,當資料為第一筆時則直接把值當作平均值,即 ,對應到程式碼中的第 6 行,注意到這邊有對 val
進行左移 avg->factor
個位元的操作,目的是預先保留 avg->factor
個位元當作小數部分,避免之後進行平均值運算時丟失小數資訊。
而當資料不為第一筆時,則需計算新資料與歷史資料的權重,即 ,對應到程式碼的第 4, 5 行,這邊針對新資料 val
也有先左移 avg->factor
個位元,另外觀察這行表示式 (((avg->internal << avg->weight) - avg->internal) + (val << avg->factor)) >> avg->weight
可以拆成:((avg->internal << avg->weight) - avg->internal) >> avg->weight
和 (val << avg->factor) >> avg->weight
相加,分別對應到 和 的運算。
因為 EWMA 的計算過程中為了保留小數部分有將新資料都先左移 avg->factor
個位元再進行運算,因此若要讀取計算完成的 EWMA,需要將 avg->interval
右移 avg->factor
個位元校正回來。
在 /include/linux/average.h 中定義了巨集 DECLARE_EWMA
用以進行 EWMA
的運算。裡面定義的函式 ewma_##name##_init
、ewma_##name##_read
和 ewma_##name##_add
都有進行以下的確認才進行後續的操作:
首先透過 BUILD_BUG_ON(!__builtin_constant_p(...))
可以確保 _precision
和 _weight_rcp
在編譯時期都是常數。
BUILD_BUG_ON((_precision) > 30)
用以確保 _precision
的值不會大於 30,根據註解說明其目的是為了避免過多位元都被當作小數部分,不禁好奇為甚麼是使用 30 作為分界呢? 但是翻過歷史 commit 紀錄,都沒有說明使用 30 的原因。
另外我發現,在 Linux 核心原始程式碼中的 _precision
功能與測驗題的 factor
相同,但是比起 factor
,_precision
的命名方式更可以讓我明白它的用途,而 commit eb1e011 做的變更就是為了這個目的把 _factor
改成 _precision
。
測驗三的 ilog2
計算的是 ,而測驗五的 ceil_ilog2
計算的是 。在作法上,ilog2
需要求該數字的二進位表示中,最高位元為 1 的索引值,其索引從 0 開始;而 ceil_ilog2
的作法則需要分成兩種情況探討,一種是其值為 2 的冪的情況,在這個情況下作法同 ilog2
,另外一種則是值不為 2 的冪,其作法同 ilog2
但是最後需要再加上 1 。
一開始 x--
的操作用以處理 x
為 2 的冪的情況,使其不再是 2 的冪,因此便可以採取處理不是 2 的冪的方式進行計算,且最終也會得到正確的結果,因此接下來的操作適用於任何大於 0 的整數。
接下來的操作與 ilog2
找最高位元為 1 的方式大同小異,都是採取二分搜尋法,r
用來存最高位的索引值,不過與 ilog2
不同的地方在於,這邊都是使用位元運算子來進行操作,可以達到 branchless ,提高效能。