contributed by < 2020leon >
1
本題之函式均在求二整數之平均值。
1
填空觀察陳述式 (a >> 1) + (b >> 1) + (EXP1)
可以了解到 EXP1
的功能為判斷其是否進位,即其範圍為零到一之間。將奇數和偶數分別代入變數 a
和變數 b
後可發現,唯一需要進位的情況為 a
和 b
同時為奇數之時。
由於現代電腦是以二進位表示數值,數值之最低有效位可用於判斷整數之奇偶性。目前目標為分別「兩個變數同時為奇數」及「其他」以上二者,因此使用 &
運算子;又僅需取出最低有效位,因此再與 1
做 &
運算。 EXP1
應填入 a & b & 1
。
首先觀察 EXP3
,對其進行右移一的運算可推得其可能與類似加號的位元運算有關,而 XOR 可是為位元和,因此 EXP3
應填入 a ^ b
。
然 a ^ b
未考慮進位問題,我們可以觀察到當兩個位元均為一時才需進位。在加法時可用 (a & b) >> 1
表示 a + b
的進位。而本題旨在計算平均,因此應為 (a & b) >> 1 << 1
。所以 EXP2
應填入 a & b
。
陳述式 | 答案 |
---|---|
EXP1 |
a & b & 1 |
EXP2 |
a & b |
EXP3 |
a ^ b |
1
組合語言將所有在測驗 1
名為 average 的函式以 gcc -S -masm=intel -Os foo.c
化為組合語言,並去蕪存菁,僅保留指令部份。
首先是相加除以二的版本。
以下是未去蕪存菁, gcc
的原始輸出:
lea
即「 load effective address 」,後者為所謂「 effective address 」,中文謂之「有效位址」,其功能為將後者做完運算後賦值予前者。此例即 eax
的值會被賦為 rdi + rsi
。
shr
即「 shift right 」,將暫存器內的值向右移一位後存回。此例即將 eax
的值除以二。
ret
即「 return from procedure 」,回到原呼叫函式之處。
接著是避免 overflow 版。
此處指令命名皆直觀易解,即 eax = esi - edi
, eax >>= 1
, eax += edi
,最後結果存於 eax
。
接著是第一次改寫版。
第三個指令實做 a & b
並存在 edi
,第四和第五個指令實做 a >> 1
和 b >> 1
並存回原位,第六個指令實做 edi & 1
並存回 edi
,第七和第八個指令將三者相加後存在 eax
。
最後是第二次改寫版。
第二個指令實做 a & b
並存於 edi
,第三個指令實做 a ^ b
並存於 eax
,第四個指令實做 eax >> 1
並存回原位,第五個指令將二者相加後存於 eax
。
在此可觀查到此版本比上一個版本的指令數還少。
include/linux/average.h
include/linux/average.h
內定義一個名為 DECLARE_EWMA
的巨集以實現固定精度的 EWMA 演算法。 EWMA 演算法求取移動平均﹐並對越久的歷史資料給予越低的權重。如下式。
其中 是在時間 上的 EWMA 值, 是在時間 上的值, 則是一個介於 0 和 1 的參數。
在來看 DECLARE_EWMA
的部份。
大致觀看程式碼可發現該巨集可謂「程式碼產生器」,產生一個結構及數個函式。再看巨集參數的部份,第一個參數為 name
,可為巨集中的結構即函式命名;第二個參數為 _precision
,定義小數精度,其單位為位元;第三個參數為 _weight_rcp
,即上方公式所提及之 的倒數,而其須為二的冪。
接著詳閱 DECLARE_EWMA
巨集。
在此定義一個名為 ewma_##name
的結構,內部僅定義一個 unsigned long
的成員。
注: ##
即串接。若將 name
帶入 a
,則結構名為 ewma_a
。
接著是 ewma_##name##_init
函式。
內部可發現名為 BUILD_BUG_ON
的巨集,其定義在 include\linux\build_bug.h
中,其功能為在編譯時期確認其參數是否為真,若為真則停止編譯。其中還可發現名為 __builtin_constant_p
的 gcc
內建函式,其功能為確認該參數是否為編譯時期常數。
該函式唯一的功能為將結構中的成員設為 0 ,且須在呼叫以下函式之前被呼叫。
再來是 ewma_##name##_read
函式,其功能便是讀取 EWMA 值的整數部份。
最後是 ewma_##name##_add
。
在函式內的實做可以發現 READ_ONCE
和 WRITE_ONCE
巨集。此二巨集是為了避免編譯器過度優化,即避免其合併指令和調換順序。
此函式的功能為藉由 val
算出下一個 EWMA 值並存回結構的 internal
中。其實做關鍵之處為 (((internal << weight_rcp) - internal) + (val << precision)) >> weight_rcp
,其利用位移的技巧巧妙地算出下一個時間的值。
其在 Linux 核心內的應用如 net/mac80211/sta_info.h
等,與計算來回通訊延遲有關。
2
2
填空在填空之前,先嘗試將上述改成有分支的實做。
再嘗試展開。
最後經過比較後,分別於 EXP4
和 EXP5
中填入對應的程式碼。
陳述式 | 答案 |
---|---|
EXP4 |
a ^ b |
EXP5 |
a < b 或 a <= b |
2
branchless 實作即如下。
arch/x86/kvm/mmu.h 為其中一個 branchless 實做。
3
3
填空首先第一句 if
陳述式用以判斷所傳入之參數是否為零,若其中之一為零則回傳另一參數之值。即 、 和 。
接下來的 for
迴圈判斷所傳入之參數是否皆為偶數,若皆為偶數則將其同時除以二,並以 shift
記錄迴圈次數(即除以幾個二)。在經過 for
迴圈後,即可保證 u
和 v
其中之一為奇數,且可以推論經過處理過的 u
和 v
目前最大公因數為奇數。以上對應到以下數學式: 。
由於目前最大公因數為奇數,再來的 while
迴圈則將 u
關於二的因數去除。( )
緊接著的 do
while
迴圈的第一步則同上方的 while
迴圈,只是針對不同的變數做操作。再來的 if
else
則是將二數相減,取其差的絕對值存入 v
,取二數較小者存入 u
,即輾轉相除法。
接著是 do
while
的迴圈條件 COND
。輾轉相除法的結束條件為其中之一為零。由於 v
為二數之差,故以 v
作為檢查條件,而 u
則為經過處理過的二數之最大公因數。
最後在回傳值的部份需將偶數部份還原,也就是回傳 u << shift
。
陳述式 | 答案 |
---|---|
COND |
v |
RET |
u << shift |
嘗試用 __builtin_ctz
改寫 GCD 。
lib/math/gcd.c 中其中一個實做如下。
其中 __ffs
為 find first set ,在此功能同 ctz
相關函式。
r & -r
即對於自己的二補數(一補數加一)進行 AND 運算,所以其結果為 1 << __builtin_ctzl(r)
,必為二的冪,可類比上方實做的 shift
。
其餘邏輯與上方實做相似。
在核心內的應用如 lib/math/lcm.c 、 sound/core/pcm_timer.c 等。
4
4
填空題目提及如下:
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到
t
變數。
我們可以利用二補數的特性來完成 EXP6
的需求。考慮到 xx100
( x
代表 don't-care term ),其二補數亦為 xx100
。兩者做 AND 運算後亦為 xx100
故填入答案 bitset & -bitset
。
陳述式 | 答案 |
---|---|
EXP6 |
bitset & -bitset 或 -bitset & bitset |
5
5
填空陳述式 | 答案 |
---|---|
PPP |
pos-- |
MMM |
list_add |
EEE |
&heads[remainder % size] |
6
6
填空可以觀察到其是利用 struct
對齊性質來實做 alignof
。首先觀察 M
,可發現其接在 ->
運算子之後,所以應為 c
或 _h
其一;又若填入 c
則 &((struct { char c; t _h; } *)0)->c
恆為零,無法體現出 t
的型態,因此填入 _h
。而事實上 &((struct { char c; t _h; } *)0)->c
的值已是正確答案,將 X
填入 0
則不會影響結果。其回傳型態為 ptrdiff_t
。
陳述式 | 答案 |
---|---|
M |
_h |
X |
0 |
ALIGN
, ALIGN_DOWN
, ALIGN_UP
等巨集ALIGN
、 ALIGN_DOWN
在 include/linux/align.h 可見; ALIGN_UP
則在 tools/testing/selftests/vm/pkey-helpers.h 可見。
7
7
填空上方程式碼的 length
變數表示 fmt
的長度。由規則可知,若為三的倍數需印出 Fizz 、五的倍數需印出 Buzz 、十五的倍數需印出 FizzBuzz 、其餘印出該數。因此可製成下表。
數值 | length |
---|---|
三的倍數 | 4 |
五的倍數 | 4 |
十五的倍數 | 8 |
其他 | 2 |
由上表可推論 (2 << KK1) << KK2
為 (2 << div3) << div5
或 (2 << div5) << div3
。
接下來觀察 (9 >> div5) >> (KK3)
的值。
數值 | (9 >> div5) >> (KK3) |
KK3 |
---|---|---|
三的倍數 | 0 | 4 |
五的倍數 | 4 | 0 |
十五的倍數 | 0 | 4 |
其他 | 8 | ?(0) |
校:上方程式碼之 (9 >> div5) >> (KK3)
應為 (8 >> div5) >> (KK3)
,否則該印數字之處會錯誤印出 u
字樣。
可知 KKK3
為 div3 << 2
。
陳述式 | 答案 |
---|---|
KK1 |
div3 |
KK2 |
div5 |
KK3 |
div3 << 2 |