contributed by < Wallmountain >
原始程式碼, 用以取得兩個無號整數的平均
改善 overflow 問題的版本
接著回答以下等價的實作
若不看 EXP1 的函式輸出
a | b | average result |
---|---|---|
1 | 1 | 0 |
3 | 4 | 3 |
4 | 4 | 4 |
5 | 11 | 7 |
會發現 a
, b
最低位元因右移被捨棄, 因此必須要去判斷是否同時為1,若是, 則回傳值加1
EXP1 = a & b & 1
接著為下一個等價的實作
可分成兩個部分
a
, b
在同一位元同時為1,相加會進位, 但因為右移1, 故不進位 -> EXP2 = a & b
a
, b
在同一位元只有一方為1,在同一位元留下1,接著右移1 -> EXP3 = a ^ b
用 gcc -S -Og test1.c
得到, 只比較 main 中實作的差異, 故得到以下組合語言
參考 What does the endbr64 instruction actually do?, endbr64 stands for "End Branch 64 bit". The instruction is otherwise considered a NOP
leal
這個指令屬於 load effactive address
的指令, 雖然為 load
指令, 但很常被用來當成 add
指令使用,以上可視為將 a
和 b
兩個值相加並存入 %eax
中
shrl
為右移1,可視為除以二
根據 ret 裡面所說, The ret instruction transfers control to the return address located on the stack, 相當於 return
第2, 3兩行為 high - low
並將結果存入 %eax
shrl
同樣為右移1,可視為除以二
第5行為最後加上 low
movl
和 shrl
的組合分別為 a >> 1
和 b >> 1
接著, 將 a >> 1
和 b >> 1
相加
連續的 andl
為 a & b & 1
最後將上面結果相加
movl
和 andl
的組合為 a & b
xorl
為 a ^ b
shrl
將 a ^ b
右移1
最後將上面結果相加
解釋程式碼
這邊先思考 max 會用到的回傳值,比較 a, b 的大小, 回傳較大的, 也就是只會回傳 a 或 b
a ^ a ^ b = b
, 故思考後面的判斷式的用意
a < b
成立時, (a ^ b) & -(a < b) = a ^ b
, 函式回傳 a ^ a ^ b = b
a < b
不成立, (a ^ b) & -(a < b) = 0
, 函式回傳 a ^ 0 = a
針對 32 位元無號/有號整數,撰寫同樣 branchless 的實作
無論 a, b 為有號還是無號, -(a < b)
的結果都一樣
a < b
, -(a < b) = 0xFFFFFFFF
-> a ^ a ^ b = b
a >= b
, -(a < b) = 0x00000000
-> a ^ 0 = a
所以在實作上除了 type, 並無二致
原64 位元 GCD
改寫為以下等價實作
對照 GCD 演算法
對照到演算法第2點, 且只有除數有可能為 0, 故離開 do while 迴圈的條件為 v
類似 輾轉相除法, 大減小, 減完後大的當被除數
最後被除數要補償根據演算法第三點多除的
在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
利用 __builtin_ctz
取代以下 while 迴圈
shift 則用測驗2所學實作 min 的功能, 避免使用 branch
在 main 中使用 for 迴圈重複呼叫函式以測試效能改善
Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
__builtin_ctz
r & -r
代表只剩 r 從最低位元開始的第一個1, 剩下為0, 也就是 1 >> __ffs(r)
, 而在 for 迴圈中的 r & -r 可改寫為 a << __ffs(r)
,可將兩個 if 合成一個 if, 讓程式碼更簡潔在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼
for (int i = 0; i < 64; i++)
避免逐個bit去搜尋用 GNU extension 的
__builtin_ctzll
以改寫的程式碼如下:
改善逐個 bit 搜索, 和避免 branch
bitset & -bitset
用以找到 bitset
從最低位元的 bit 開始第一個為1的 bit, 接著使用 XOR 將其去掉得知從最從最低位元的 bit 開始第一個為1的 bit 距離幾個 0
跟原程式碼比較, 因為沒有逐個 bit 計算, 故使用 __builtin_ctzll
才能知道用 bitmask 得到的 bit 前面有幾個0
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作
find
函式就是利用 key 去 hash table 中尋找是否 key 存在在 heash tableint pos = find(heads, size, remainder);
用來尋找是否前面有出現過相同餘數, 若找到則進入 branch指出其中不足,並予以改進
listallfree
函式釋放 map 記憶體sprintf(p, "%ld", (long) division);
代替 sprintf(p, "%ld", division > 0 ? (long) division : (long) - division);
, 前面有將被除數和除數都變為正值, 故這邊不應出現負值decimal
字串來進行實作, 最後再將 decimal
寫入 result
, 故思考將 decimal
去除, 直接對 result
做操作, 利用 z
變數紀錄當前在字串位置, 若發現為循環小數, 則從字串尾巴開始改寫至小數非循環的部分__alignof__ 是 GNU extension,以下是其可能的實作方式:
這邊用來計算取得 t
在 structure 中需要對齊幾個位元, 故為 (char *)(&((struct { char c; t _h; } *)0)->_h)
, 後面減去 (char *)0
貌似簡單卻蘊含實作深度的 FizzBuzz 題目
直覺的實作程式碼
利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼
解釋上述程式運作原理
9 >> 4 = 0
9 >> 1 = 4
9 >> 5 = 0
9 >> 0 = 9
用改變字串寫入長度和位置, 來得到輸出字串