contributed by < ppodds
>
考慮以下對二個無號整數取平均值的程式碼:
改寫為
第二種寫法相當於
很顯然我們要補的就是被floor捨去掉的值。
舉個簡單的例子
我們必須在 a 和 b 同時是奇數時補 1 給最終結果。寫法如下:
(a & 1) & (b & 1)
a & b & 1
故 EXP1 為 a & b & 1
改寫為
限定使用 OR AND XOR
直覺想到用加法器的概念做
加法 XOR
進位 AND
先把 a + b
拆成用加法器的 bitwise 形式
a + b = a ^ b + (a & b) << 1
補上除2答案就出來了
(a + b) / 2 = (a & b) + (a ^ b) >> 1
故 EXP2 為 a & b
, EXP3 為 a ^ b
延伸問題:
移動平均(Moving average),又稱滾動平均值、滑動平均,在統計學中是種藉由建立整個資料集合中不同子集的一系列平均數,來分析資料點的計算方法。
移動平均通常與時間序列資料一起使用,以消除短期波動,突出長期趨勢或周期。短期和長期之間的閾值取決於應用,移動平均的參數將相應地設置。例如,它通常用於對財務數據進行技術分析,如股票價格、收益率或交易量。它也用於經濟學中研究國內生產總值、就業或其他宏觀經濟時間序列。
比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
avg.c
編譯指令 gcc avg.c -O2 -o avg
objdump -D -M intel avg > avg.dump
先備知識
rdi 和 rsi 是 gcc x64 calling convention 的前兩個參數
lea 將後者 address 存的值放到前者之中,且允許後者可以進行暫存器運算
根據上面的解釋,可以看出第一行其實在執行 a + b
並把結果存入 eax
中。下一行的 shr eax,1
即是 eax / 2
最終做出 (a + b) / 2
mov eax,edi
、 mov edx,esi
先各做了一份 a 和 b 變數的備份,待會算 a & b & 1
會用到。接下來直接做 a & b
存到 edi
中,並執行 a >> 1
與 b >> 1
。此時才會再把 edi & 1
(在此可以看出 compiler 在優化時有重排我們的程式碼),最後把前面計算出來的東西用 add
加起來。
先備份 a 到 eax
後把 edi
設為 a & b
,再把 eax
設為 (a ^ b) / 2
,最終把兩個結果加在一起。
研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
大致上可以理解為一個簡單的動態平均系統,一開始先設置一個權重,之後每次計算時根據權重和新進來的數值做加權平均。
include/linux/average.h
定義了一個大micro DECLARE_EWMA(name, _precision, _weight_rcp)
。可讓 user 根據傳入的參數產生 EWMA 的 struct 和操作函式。分別為 ewma_##name
(struct) 、ewma_##name##_init
、 ewma_##name##_read
、 ewma_##name##_add
。
從上面的 code 可觀察出 linux 在 compile time 時就有對巨集傳入的參數做檢查,來避免 runtime 時的錯誤。
##name##會被代換成巨集的 name 參數
ewma_##name##_init
初始化 struct 的內容(把 internal 設成 0)
ewma_##name##_read
讀取 EWMA 當前的數值
ewma_##name##_add
加入新的一筆資料進到 EWMA 中
read
和add
剛好對應到 EWMA 的基本操作
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
由於題目有一點難度,需要慢慢把思考過程寫下來。
首先從EXP5開始猜測, EXP5 是一個 a 和 b 的比較,意思是只會傳回 true
或 false
,即 1
或 0
。經過負號轉換後 1
變為 -1
,即 0xFFFFFFFF
。到此為止可以稍微有點頭緒, (EXP4) & -(EXP5)
在 EXP5 的條件滿足時,會把 EXP4 的數值完整留下來,否則結果會是 0
。當此敘述為 0
時,經過和 a
的 XOR 運算,結果會是 a
。固可先猜測 EXP5 在 a < b
時會滿足。
接下來考慮 EXP5 滿足的情形。當 EXP5 滿足時,我們需要把 b 作為最終結果回傳回去。簡化後可以看成 a ^ (EXP4)
要等於 b
。回想 XOR 的特性,某數對同樣數字 XOR 兩次以後會得到自己,因此可猜測 EXP4 為 a ^ b
。
EXP4
= a ^ b
EXP5
= a < b
延伸問題:
針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
在這裡只要 a
和 b
同時是有號數或無號數結果就會正確(不一樣的話會造成 a < b
的比較與預期不同)。
考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式:
改寫為以下等價實作:
- if both x, y are 0,
- and
剛好對應上
- if x, y are both even,
只要 u
和 v
都是偶數,就除二並把 shift
加 1
(此處用 last bit 是否等於 1
來判斷奇偶)。當迴圈結束時,會剛好達成 (此處的 u
和 v
指的是函式剛傳進來時的值)。
- If x is even and y is odd, then
- If x is odd and y is even, then
此時已經不能再提出 2
的公因數了,因此可以把 u
和 v
都除 2
到變成奇數為止。
對到最後的剩下的兩條。
最後的 do while 迴圈中止條件和一般的 gcd 一樣是 v=0
,故 COND = v
,而回傳值如上面所述, RET = u << shift
COND = v
RET = u << shift
在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
考慮以下程式碼:
改寫的程式碼如下:
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
請補完 EXP6
-bitset
會將 bitset
最右邊的 1
前方的所有 bits 都翻轉(二補數),和 bitset
做 AND 後就會只剩下最右邊的 1 的 bit。故 EXP6=bitset & -bitset
。
EXP6=bitset & -bitset
解釋上述程式運作原理,並舉出這樣的程式碼用在哪些真實案例中
設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density
思考進一步的改進空間
閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作:
請補完
PPP = ? (表示式)
MMM = ? (巨集)
EEE = ? (表示式)
PPP=
MMM=
EEE=
alignof 是 GNU extension,以下是其可能的實作方式:
請補完上述程式碼,使得功能與 alignof 等價。
這題的答案我不確定
M=_h
X=0
將 0
轉型成 struct { char c; t _h; }
的 pointer,並取得 -h
的 address,會拿到 -h
在結構中的 offset,再扣掉一開始 struct base address 的位置。
不解的是為什麼要扣掉 base address ,如果 base address 已經是 0 了應該不需要再扣一次。
在 Linux 核心原始程式碼中找出 alignof 的使用案例 2 則,並針對其場景進行解說
在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
- 從 1 數到 n,如果是 3的倍數,印出 “Fizz”
- 如果是 5 的倍數,印出 “Buzz”
- 如果是 15 的倍數,印出 “FizzBuzz”
- 如果都不是,就印出數字本身
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
作答區
KK1 = ? (變數名稱)
KK2 = ? (變數名稱)
KK3 = ? (表示式,不包含小括號)
KK1=div3
KK2=div5
KK3=div3 << 2
原理是透過位元運算湊出 fmt
(要輸出的字串)數值
fmt
的決定方法是由 &"FizzBuzz%u"[(9 >> div5) >> (KK3)]
和 length
共同控制。第一項是字串的開頭,後面是持續長度,列出表以後可以求出上面三條式子。
9
怪怪的,要改成 8
,不然吃不到 %u
,最後印出來的是 u
解釋上述程式運作原理並評估 naive.c 和 bitwise.c 效能落差
- 避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
分析 Faster remainders when the divisor is a constant: beating compilers and libdivide 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless;
- 參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware
研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論