contributed by < ShiaoLin >
1
對二個無號整數取平均值的程式碼,我們可改寫為以下等價的實作:
a
與 b
皆為無號數,向右邏輯位移 1
位乍看之下等同於兩數的一半相加,在算術上的邏輯是成立的,但是考量到 bits
的表達方式,a
與 b
各自向右移位會導致 bit 0
被忽略掉,在此之上要補回對於 bit 0
的運算。
以下以無號數 4 位元簡單表示:
Raw | shift right | |
---|---|---|
a | 1 0 0 1 | 0 1 0 0 |
b | 1 0 0 0 | 0 1 0 0 |
result | x | 1 0 0 0 |
上述表格透過直接右移一位於結果來說沒有問題,但是下面表格就會出現錯誤了
Raw | shift right | |
---|---|---|
a | 1 0 0 1 | 0 1 0 0 |
b | 1 0 0 1 | 0 1 0 0 |
result | x | 1 0 0 0 |
可以看到我們預期 a
與 b
的平均結果應該是 1001
,所以我們必須對於 bit 0
進行修正,也就是加上 a & b & 1
去檢查兩數 bit 0
的狀況
我們再次改寫為以下等價的實作:
我們可以透過 a + b
= (a & b) << 1 + a ^ b
進行操作,也就將 (a + b)
進行邏輯右移取得平均值,也就是 a & b + (a ^ b) >> 1
使用指令進行編譯器最佳化的觀察
會得到 filename.s
這個編譯到一半的檔案,其中可以觀察各程式轉變為組合語言的指令對應
首先觀察最一開始的取平均做法
會得到如下的結果
endbr64
的解釋What does the endbr64 instruction actually do?
leal (%rdi,%rsi), %eax
,表示將 (%rdi,%rsi)
處理完後的地址加載到 %eax
這個地址去,lea
指令不儘可以處理加法也可以處理乘法,相較於 add
或 mul
來說節省了一道後續 mov
指令,可以在大量的組合語言中看到 lea
的使用
shrl %eax
則是將 %eax
做右移一位的處理,等同於將值除以 2
的操作
接下來觀察改寫後的操作
會得到下列結果
得到了相對多於上述操作的組合語言指令,雖然指令上使用的較多,但是是相對獲得防止 overflow
的作法
同樣觀察下列操作的組合語言
同樣防止溢位,此次操作又比上述的操作少了一些指令
根據移動平均的定義,相較於一般的平均數的操作,移動平均會對於新加入的值提升權重,使新值影響移動平均較大,以下是核心定義設時間 的實際數值為 ,而時間 的 EMA
則為;時間 的 EMA
則為 ,計算時間 是方程式為
接下來觀察 linux
中相關的 EMA
程式碼,透過 macro
宣告包含初始化結構體、讀取結構體的值以及增加新值至原先結構體的方法
這邊有 3
個參數, name
表示結構體的命名, _precision
表示用多少 bits
儲存值的小數部分,_weight_rcp
表示為加權數,這邊為 的倒數
這邊觀察到程式碼內大量使用的以下的錯誤檢查式
其中 BUILD_BUG_ON
表示括號內的表達數如果出現 false
則強制讓編譯器出現錯誤,BUILD_BUG_ON
的原定義來自於 BUILD_BUG_ON_ZERO
,相關操作可參bit-field的說明
READ_ONCE
以及 WRITE_ONCE
是為了避免不同 thread
同時進行操作導致數值存取錯誤的問題發生,ilog2
則是返回某數以 2
為底的整數部分,以利後續使用位移進行快速操作方便,這段程式碼的關鍵就在於 WRITE_ONCE
中的參數其中的分支,若 internal
為 0
,則 e->internal = val << precision
,等同於,而 internal
不為 0
時,等同於完整的表達式
2
利用 XOR
的特性思考:
a ^ 0 = a
a ^ a = 0
a ^ (a ^ b)
因具備結合律等同於 (a ^ a) ^ b = b
假設 EXP4
為 a ^ b
,則 -(EXP5)
則必須具備表達 a
與 b
的大小關係式
進一步假設如果 EXP5
為 a > b
或 a >= b
時:
a >= b
為真, return a ^ ((a ^ b) & -1)
其結果為 return b
,假設與結果不符a < b
為真, return a ^ ((a ^ b) & 0)
其結果為 return a
,同樣假設與結果不符故得知 EXP5
的正確表達式為 a < b
參考不需要分支的設計將有 32 位有號數較小值的函式作修改
3
根據備註 3. If x and y are both even, gcd(x,y)=2∗gcd(x2,y2) because 2 is a common divisor. Multiplication with 2 can be done with bitwise shift operator.
若 u
v
兩數皆為偶數則先共同約去 2
,用 shift
紀錄約去的次數
根據備註 4. If x is even and y is odd, gcd(x,y)=gcd(x2,y).
如果 u
為偶數,則可先約去 2
的冪部份,因為與奇數的最大公因數不會有 2
的冪存在
根據備註 5. On similar lines, if x is odd and y is even, then gcd(x,y)=gcd(x,y2). It is because 2 is not a common divisor.
同樣的,對 v
進行備註 4 的處理方式後,就可以就剩下的數字進行最大公因數的尋找,這邊參考輾轉相除法,而輾轉相除法的停止條件是餘數為零,也就是 u - v
的結果,故 COND = v
,而最後回傳值除了剩餘的 u
之外,還要包含上方若有對 2
進行約分的部份,也就是 RET = u << shift
__builtin_ctz
改寫 GCD,分析對效能的提升4
思考 -bitwise
的特性,-bitwise = ~bitwise + 1
,所以當 bitwise & -bitwise
時會留下 bitwise
最靠近右端的 1
,最後再透過 bitwise ^= t
去掉最右端的 1
EXP6
為 bitset & -bitset
5
在 hash table
中的元素建立資料,第一次進入 for
迴圈時由於 pos
從 find
的回傳值為 -1
,所以會進行新增節點的動作,故 MMM
為 list_add
,而節點的插入點 EEE
為 &heads[remainder % size]
而 PPP
與 pos
相關,當循環小數不從小數點後一位開始時, *p++ = *decimal++;
會先記錄循環小數開始前的數字,故 PPP
為 pos--
6
參考 container_of
的巨集,以 char*
指針形式回傳的 struct { char c; t _h; } *
結構體指針,透過 0
指針指向 t
型態的地址,再減去作為開頭 char* 0
的偏移量,故 M = _h
,X = 0
7
根據 string literal
的長度思考:
字串長度 | Offset |
|
---|---|---|
3 非5 的數 |
4 | 0 |
5 非3 的數 |
4 | 4 |
3 且5 的數 |
8 | 0 |
非3 非5 的數 |
2 | 8 |
觀察結果發現不論是字串長度與 Offset
皆為 2
的冪,所以剛好符合 KK1 = div3
及 KK2 = div5
再來關於 (9 >> div5) >> (KK3)
的部份,套入上表格可得到
字串長度 | (9 >> div5) >> (KK3) |
KK3 |
|
---|---|---|---|
3 非5 的數 |
4 | 0 | 3 |
5 非3 的數 |
4 | 4 | 0 |
3 且5 的數 |
8 | 0 | 2 |
非3 非5 的數 |
2 | 8 | 0 |
題目有誤,應修正為 (8 >> div5) >> (KK3)
且 KK3 = div3 <<2