contributed by < SmallHanley
>
1
解釋上述程式碼運作的原理
考慮以下對二個無號整數取平均值的程式碼:
這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow:
接著我們可改寫為以下等價的實作:
原理:
如果是我們平常使用的兩實數取平均,可以寫成:
而上述的程式碼考慮的是非負整數,因此我們可以使用右移運算子 >>
代替上式的除以 2。不過這還不是等價的程式碼,會忽略到兩數都是奇數時的進位問題,因此要加上 a & b & 1
,使得當兩數都是奇數時會再加一。
我們再次改寫為以下等價的實作:
原理:
上述程式碼讓我們很容易聯想到加法器,我們先觀察一下半加器,以下是真值表 ({C, S} = A + B):
A | B | C | S |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 |
1 | 0 | 0 | 1 |
1 | 1 | 1 | 0 |
得到以下結果:
而全加器則是除了兩個一位元的輸入訊號,還需要低一位的進位訊號 (即 {Cout, S} = A + B + Cin)。當我們要計算多位元的非負整數加法,可以利用多個全加器串接 (或稱級聯,cascade)。若要使用到半加器的結果,當前位數的運算結果,可以看作是當前位數的兩輸入訊號相加再加上前一位的進位訊號 (即 S + Cin,注意,這裡的 S 為半加器的 Sum,Cin 為前一位的 Cout),也就是當前位數兩輸入訊號的 bitwise XOR 加上 前一位數兩輸入訊號的 bitwise AND。因此,兩多位數 (分別為 a 和 b) 相加的結果可以寫作:
兩數平均可以寫作:
比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
暫略,我目前沒有觀察到什麼有趣的現象。
研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
在 include/linux/average.h 中使用巨集 DECLARE_EWMA(name, _precision, _weight_rcp)
用來生成相關的物件和函式。其中,_precision
是用來表示小數部份的精準度,也就是我們使用多少位元表示。而 _weight_rcp
則是權重。
而巨集內部包含以下結構體:
以及以下函式:
internal
初始化為 0
。internal
右移 _precision
的位元數回傳。val
新增進來,新的 internal
會是舊的 internal
和 val
的線性組合 (val
佔 1 / _weight_rcp,舊的 internal
佔 1 - 1 / _weight_rcp)。internal
佔比的部份,原本應寫作:應用:
比方說 drivers/net/wireless/realtek/rtw88/main.h 有使用到:
可以推測跟一些訊號處理有關係。
2
解釋上述程式碼運作的原理
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min
,我們得到以下實作 (max
):
原理:
我們知道 a 和 b 做 bitwise XOR,再和 a 做 bitwise XOR 會得到 b,因此我們可以運用這個原理讓 a 和 (a ^ b) 或是 0 做 bitwise XOR 而分別得到 b 和 a。至於要怎麼做到不使用分支,我們可以利用 a < b 的結果,加上負號,會產生兩種結果 (0x00…00 和 0xff…ff),再加上 bitwise AND 可以巧妙的做到無分支判斷最大值。
針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
當我們要判斷一個 32 位元無號整數是否為 2 的冪,我們可以用以下程式碼判斷:
我們可以改寫成以下 branchless 的實作:
Linux 核心也有若干 branchless / branch-free 的實作,例如 lib/sort.c:
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log
檢索。
在 Replace int2float() with an optimized version. (commit 747f49ba) 中,將 32 位元整數的 bit pattern 轉成浮點數的 bit pattern,有使用到類似的實作手法 (不過目前 r600_blit 相關程式已背棄用)。以下是 IEEE 754-2008 定義的 binary32
格式,其中藍、綠、紅分別代表 sign、exponent 和 fraction,分別佔用 1、8、23 位元:
完整實作如下:
其中用到一個 ror32
的函式,是被定義在 include/linux/bitops.h,程式碼如下:
有些指令集架構有支援 rotate 指令,比方說 x86_64 指令集 具備 ror
、rol
指令,上述程式碼經過編譯器最佳化,確實會產生出 ror
指令。
新版 int2float
的程式碼先使用 __fls
來找出該整數的指數部份。原本的實作 fraction 會用左移加上條件判斷來求值,新版使用 (msb - I2F_FRAC_BITS) & 0x1f)
,搭配 ror32
來實作,減少檢查是否大於 所需的分支,前者利用 unsigned integer 減法的溢位來決定輸入小於 時的位移數目,搭配後者 rotate 指令,不管輸入大於或是小於 ,皆可以做到保留 MSB 之後的 23 個位元給 fraction
的效果。
3
解釋上述程式運作原理;
考慮以下實作:
性質:
一開始的 if 判斷式對應到性質 1、2,如果 x、y 皆為 0,回傳 0;如果 y 為 0,回傳 x;如果 x 為 0,回傳 y。接著,for 迴圈對應到性質 3,並用 shift
記錄乘了多少次 2。while 迴圈對應到性質 4。do while 迴圈則是用到性質 5 以及 Euclid's_algorithm。做到最後 u
和 v
會相等,會跳到 else 區塊,做完該區塊後 u
為一非 0 數值,v
為 0,跳出迴圈,再將 u
左移 shift
。
4
解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中;
在影像處理中,bit array (也稱 bitset
) 廣泛使用,考慮以下程式碼:
考慮 GNU extension 的 __builtin_ctzll
的行為是回傳由低位往高位遇上連續多少個 0
才碰到 1
。
範例: 當
a = 16
16
這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000
從低位元 (即右側) 往高位元,我們可發現 ,於是 ctz 就為 4,表示最低位元往高位元有 4 個0
用以改寫的程式碼如下:
在 naive()
這個函式中,需要將 bitmap 中的所有 1
的位置記錄到 out
中,並回傳 pos
代表 out
的 size。其中,我們可以使用 __builtin_ctzll
優化,如 improved()
函式所示。使用 ctz
找到目前 bitmap 中的 least significant 1
,並將該 1
clear,進行下一次迭代。我們可以觀察到 bitset ^= t
這一行程式就是 clear bit 的操作。至於 t = bitset & -bitset
則是利用二補數的特性 (可觀察 解讀計算機編碼 的圖),取二補數相當於對原數作 bitwise NOT 後加 1。二補數再和原數作 bitwise AND 可以得到一個數,該數的第 r 個 bit 為 set,其餘為 clear (等價於 1 << r
)。
設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
思考進一步的改進空間;
可改寫成以下等價實作:
閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例;
Linux 的 kernel/sched/sched.h,有關 RT scheduling 就有使用到 bitmap:
搭配下述程式碼可以找出 bitmap 中的 first set: