contributed by < ChenBoSyun
>
解題思路: (a >> 1) + (b >> 1)
等同於 a / 2 + b / 2
因此推斷 EXP1 是為了加回當 a, b 都是奇數時,被向下取整捨去的 1 (0.5 + 0.5)
EXP1 = a & b & 1
解題思路: average 可以視為相加後再除以 2
我們先做出一個加法器 add, a & b
是進位的部份,因此需要左移 1,a ^ b
是沒有進位的部份,兩者相加後就是完整的加法。
現在已經跟 average 的答案很相近了,只差了一個除以 2,也就是右移 1。
EXP2 = a & b
EXP3 = a ^ b
透過 gcc -S -O3 add.c
生成最佳化後的組合語言
version1
%edi, %esi, %eax 都是常見的暫存器,分別是第一個參數,第二個參數,返回值
%edi 的首字母 e 代表該暫存器存取 32bit
movl, subl 等指令的尾字母 l 代表指令操作對象的大小為 4 byte
指令 | 指令說明 | 執行完的結果 |
---|---|---|
movl %esi, %eax |
把參數2 high 賦予給回傳值 | %eax: high |
subl %edi, %eax |
%eax 減去參數1 low | %eax: high - low |
shrl %eax |
%eax 做 1 bit 的邏輯右移 | %eax: (high - low) / 2 |
addl %edi, %eax |
%eax 加上參數1 low | %eax: low + (high - low) / 2 |
version2
指令 | 指令說明 | 執行完的結果 |
---|---|---|
movl %edi, %eax |
把參數1 a 賦予 %eax | %eax: a |
movl %esi, %edx |
把參數2 b 賦予 %edx | %edx: b |
xorl %esi, %edi |
參數1 a 跟參數2 b 做 XOR 並把結果放進參數2 | %edi: a ^ b |
shrl %eax |
%eax 做 1 bit 的邏輯右移 | %eax: a >> 1 |
shrl %edx |
%edx 做 1 bit 的邏輯右移 | %edx: b >> 1 |
xorl $1, %edi |
%edi 跟 1 做 XOR 並把結果放進 %edi | %edi: a ^ b ^ 1 |
addl %edx, %eax |
%edx 加進 %eax | %eax: (a >> 1) + (b >> 1) |
addl %edi, %eax |
%edi 加進 %eax | %eax: (a >> 1) + (b >> 1) + (a ^ b ^ 1) |
version3
指令 | 指令說明 | 執行完的結果 |
---|---|---|
movl %edi, %eax |
把參數1 a 賦予 %eax | %eax: a |
andl %esi, %edi |
參數1 a 跟參數2 b 做 and 並把結果放進參數1 | %edi: a & b |
xorl %esi, %eax |
參數2 b 跟 %eax 做 XOR 並把結果放進 %eax | %eax: a ^ b |
shrl %eax |
%eax 做 1 bit 的邏輯右移 | %eax: (a ^ b) >> 1 |
addl %edi, %eax |
%eax 跟參數 1 相加 | %eax: (a & b) + ((a ^ b) >> 1) |
解題思路: 這題會用到 XOR 的一個特性,相同的數字做 XOR 會抵銷掉,例如 x ^ x = 0
EXP5 是條件判斷式,結果為 false 或 true
當 EXP5 結果為 false 時
EXP4 & -0 = 0
a ^ 0 = a
因此推斷 EXP5 = a < b
當 EXP5 = a < b 為 true 時
EXP4 & -1 = EXP4
a ^ EXP4 需要等於 b,因為 a < b 為 true,要回傳 b
推斷 EXP4 = a ^ b
解釋下面這段 gcd 程式碼運作原理
註: GCD 演算法
- If both x and y are 0, gcd is zero
- and because everything divides
- If x and y are both even, because is a common divisor.
Multiplication with can be done with bitwise shift operator.- If x is even and y is odd,
- On similar lines, if x is odd and y is even, then It is because is not a common divisor.
if (!u || !v) return u | v;
對應到演算法 1, 2 點,當 u, v 有任一個數字為 0 時,回傳 u | v
若 u == 0, v != 0
回傳 v,同理 u != 0, v == 0
回傳 u,當 u, v 皆為 0 時回傳 0
6 ~ 8 行對應到演算法第 3 點,提出公因數為 2 的倍數
算法核心是輾轉相減法,結束條件是當其中一方減至 0
COND = v > 0
結束時,剩餘的那方就是公因數,乘上之前計算的 2 倍數的公因數
RET = u << shift
以下用兩個實際例子說明演算法運作
表格的每列是每次迴圈執行完的結果
以 u = 3, v = 5 為例
u | v |
---|---|
3 | 5 |
3 | 2 |
3 | 1 |
1 | 2 |
1 | 1 |
1 | 0 |
回傳 u << shift 即 1 << 0,符合
以 u = 21, v = 14 為例
u | v |
---|---|
21 | 14 |
21 | 7 |
7 | 14 |
7 | 7 |
7 | 0 |
回傳 u << shift 即 7 << 0,符合
Built-in Function: int __builtin_ctz (unsigned int x)
Returns the number of trailing 0-bits in x, starting at the least significant bit position. If x is 0, the result is undefined.
__builtin_ctz
可以用來把
取代成
改寫後的 gcd 程式碼
執行 100 萬次,觀察兩者的執行時間
__builtin_ctz 方式性能提昇 2.66 倍
另外嘗試用 objdump 觀察組合語言跟原始程式碼的對照
紀錄一下使用 perf record 也可以看到組合語言跟原始程式碼的對照
原始版本的組合語言
__builtin_ctz 版本的組合語言
從組合語言可以看出
原始版本的 gcd 需要 7 個 instruction
__builtin_ctz 需要 3 個 instruction,雖然 tzcnt 需要 2 個 clock cycle,但總和 clock cycle 還是比較少
上述程式碼,用來檢查 bitmap 中有哪些 bit 數值為 1,並且回傳他們在 butmap 中的位置
可以看到裡面的 for 迴圈,會逐一檢查 bit 是否為 1
若是使用 __builtin_ctzll
就不需要逐一檢查,它會回傳從尾端數過來會經過幾個 0,才會遇到第一個 1
使用 __builtin_ctzll
的程式碼
關注第 9 跟 12 行,推測這兩行的目的是把 rightmost 1 變成 0,其他部份保留原樣
再看第 12 行
bitset 想要保留非 rightmost 1
XOR 有一個特性是 x ^ 0 = x
從這兩點來看 EXP6 要做的事情就是保留 rightmost 1,把其他的部份都變成 0
EXP6 = bitset & -bitset
原理: 因為二補數的特性 -bitset = ~bitset + 1
將 bitset 拆成兩個部份觀察,rightmost 1 左邊的部份,與右邊的部份
左邊的部份不會因為加上 1 而改變
因此左邊的部份就是 x & ~x = 0,全變成 0
右邊的部份,因為原本都是 0,反轉之後加上 1,就變回跟原本一樣 x & x = x
因此 rightmost 1 會被保留,而其他 1 都變成 0
測驗五的目的是給定一個分子跟分母,計算該分數的數值並用小數形式表示,最後以字串的形式回傳,該題目最關鍵的地方在於若是循環小數,需要用小括號包住循環的部份
例如: numerator = 7, denominator = 10
7/10 =1.42857142857…
回傳結果是 "1.(428541)"
程式碼分成兩部份,小數點前跟小數點後的部份
小數點前的部份是比較基本的數值運算`
小數點後的部份,需要判斷循環小數
為了紀錄何時開始循環,用一個 hash table 來紀錄小數點後的每個數字
當找到 hash table 中有重複的數字時,代表找到循環小數
小數點部份的程式碼
13行到20行是當 remainder 不在 hash table 中
因此17行要做的事情是把 node 加進 linked list
MMM 是把 node 加進 list 的 define macro
MMM = list_add
EEE 是要加進的 list_head 指標
EEE = &heads[remainder % size]
19, 20行做除法運算
注意這邊移動的是指標 q,並沒有移動指標 decimal
再看第四行 while (PPP > 0)
就很明顯了,當第三行 if (pos >= 0)
偵測到循環小數,先取出非循環的部份
PPP = (q - decimal)
原本的程式碼有一些地方可以修改的更好
處理負數的情況,可以用 bitwise 來實作 abs 來移除 branch
abs with bitwise
sign 判斷也可以用 bitwise 來做
ALIGNOF macro 定義了結構 struct { char c; t _h; }
並將 0 轉型成該結構的指標,只要計算 t _h 成員與結構開頭的距離就可以知道 alignment 多少
M = _h
X = 0
查看 linux 原始碼實作Alignof(https://github.com/coreutils/gnulib/blob/master/lib/stdalign.in.h#L71)