contributed by < jim12312321
>
解析:
在 2 進位中左移/右移 1 個 bit ,相當於對原本的數字乘/除 2 ,因此 (a >> 1) + (b >> 1)
就相當於 。
但是要特別注意一點,整數除法是無條件捨去,因此當 a 和 b 同為奇數時,需補 1。
因此可以知道, EXP1
需要判斷 a 和 b 同為奇數時等於 1 ,否則等於 0 。
EXP1
a & b & 1
解析:
考慮 1 個 bit 的平均,可知首先要兩 bit 相加(不考慮進位),則這個操作等同取 ^
,接著要考慮是否有可能進位,這個操作等同取 &
,之後還要除 2 (對應題目中的 >> 1
) 。
操作的步數與題目要求的空格數吻合,我們可以合理猜測在 EXP2
和 EXP3
中有一個為 a ^ b
,另一個為 a & b
!
接著繼續考慮 1 個 bit 的平均,並以 1+1 為例。
依照取平均的正常流程可知步驟應為:
1.兩數相加( 1 ^ 1)
2.補上進位( (1 ^ 1) + (1 & 1) << 1)
3.除以 2( ((1 ^ 1) + (1 & 1) << 1) >> 1)
接著我們對 ((1 ^ 1) + (1 & 1) << 1) >> 1
進行化簡。
((1 ^ 1) + (1 & 1) << 1) >> 1
= (1 ^ 1) >> 1 + (1 & 1) << 1 >> 1
(乘法分配律)
= (1 ^ 1) >> 1 + (1 & 1)
(化簡)
因此根據上述化簡,我們可以得知
EXP2
a & b
EXP3
a ^ b
以下使用引數 -O3 -S -fverbose-asm
-O3
-S
-fverbose-asm
步驟解析:
movl %edi, %eax
%eax
movl %esi, %edx
%edx
andl %esi, %edi
shrl %eax
%eax
右移 1 個 bit (a >> 1)shrl %edx
%edx
右移 1 個 bit (b >> 1)andl $1, %edi
%edx
&= 1 (a & b & 1)addl %edx, %eax
%eax
+= %edx
((a >> 1) + (b >> 1))addl %edi, %eax
%eax
+= b ((a >> 1) + (b >> 1) + (a & b & 1))ret
第二小題組合語言代碼
步驟解析:
movl %edi, %eax
%eax
andl %esi, %edi
xorl %esi, %eax
%eax
^= b (a ^ b)shrl %eax
%eax
右移 1 個 bit ((a ^ b) >> 1)addl %edi, %eax
%eax
+= a ((a & b) + ((a ^ b) >> 1))ret
參考資料:
雜記︰
我其實沒有查到 x86_64-linux-gnu 的完整組合語言指令集,只能透過一些上面的參考資料去腦補指令集的操作流程,如果後續有找到會再補充上來。
TODO: 延伸問題 3
解析︰
XOR 可理解為「找不同」,因此 ((EXP4) & -(EXP5))
中的值應為 a 與最大值間相同位元處是 0 ,不同處是 1 的數字。
e.g.
1.
a = 15, b = 14
a 為最大值,因此((EXP4) & -(EXP5))
應為
2.
a = 14, b = 15
b 為最大值,因此((EXP4) & -(EXP5))
應為
由上面的例子我們可以推知,當 a > b 時,((EXP4) & -(EXP5))
應該要是全為 0 的 bitmask ,而當 a < b 時,((EXP4) & -(EXP5))
應該要是 a ^ b 的 bitmask 。
另外根據題目要求我們可以得知 EXP5
為 a
和 b
的比較操作,因此 -(EXP5)
只有兩種可能的值,即為 0 或 -1 ,而 0 就是全為 0 的 bitmask , -1 是全為 1 的 bitmask。
因此我們可以得知 EXP4
的作用為取出 a,b 間的不同, EXP5
的作用為製造 bitmask ,使特定情況下 EXP4
能發揮作用
EXP4
a ^ b
EXP5
a < b or a <= b
解析:
因為在有號數或無號數時, XOR 都能發揮「找不同」的作用,因此以下就後面的 a < b
進行討論。
在有號數的版本中情況和無號數相同,當 a >= b
應該回傳 a
,因此此時的 bitmask (原本 EXP5
的位置) 應該應該輸出 0 ,使得回傳值變成 a ^ 0
。當 a < b
應該回傳 b
,因此此時的 bitmask (原本 EXP5
的位置) 應該應該輸出 -1 ,使得回傳值變成 a ^ (a ^ b)
。
因此有號數的版本中原先 EXP5
的位置中一樣可填入 a < b
或 a <= b
達成取最大值的效果。
TODO: 延伸問題 3
解析:
首先先來看在 測驗 3 中提供的 GCD 演算法
- If both x and y are 0, gcd is zero .
- and because everything divides 0.
- If x and y are both even, because 2 is a common divisor.
Multiplication with 2 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 2 is not a common divisor.
了解完演算法後,接著就來看程式碼的實現方式!
這行對應演算法中的第 1 步和第 2 步。
這段中可以先看迴圈的中止條件 !((u| v) & 1)
,從這裡可以發現,當 u
和 v
同時為偶數時迴圈繼續,否則終止,因此此時 shift
就是用來計算直到 u
和 v
不同時為偶數時,他們兩個間的最大公因數是 2 的幾次方。對應演算法中的第 3 行。
接著這段是在把 u
中含有的 2 因數全部去除 (因為上一段中已經確定 u
和 v
沒有 2 的因數了),可以把這段對應到演算法中的第 4 行。
(當然此時我們並不能確定到底 u
或 v
是奇或偶,但在執行完這段後,我們可以確定 u
一定為奇數)
接著由於是 do-while 迴圈,因此我們先看內容最後再來看 COND
。首先根據驗算法第 5 行的做法,將 v
也變成奇數,此時可以確定 u
和 v
都是奇數。接下來的一段 if-else 就是透過相減,確保 u
是目前 u
和 v
的最大奇數公因數,因此終止條件 (COND
) 就是用來判斷是否還需要繼續找最大奇數公因數。
COND
v
而當迴圈終止時,此時的 u
就是原本 u
和 v
間的最大奇數公因數。因此要再將原先算好的兩者之間最大 2 的次方的公因數補回去。
RET
u << shift
在 x86_64 上透過 __builtin_ctz 改寫 GCD,分析對效能的提升
搭配 min
取得兩數之間最小的 ctz
。
gcd64
gcd64_ctz
Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
在 lib/math/gcd.c 中有提供兩種實作方法,但本質上沒什麼差別,只差在判斷 __ffs
能不能用。
在進入程式碼解釋及探討應用場景前,我們先來看看 __ffs
是什麼?
__ffs(unsigned long word)
的作用找到 word
中第 1 個 1 的位置,要注意當 word
中沒有任何 1 的時候, __ffs(unsigned long word)
屬於未定義行為。
參考:
__ffs
備註:
ffs (linux) 與 ffs (wiki) 中有一點落差,直接看例子能直觀的感受。
e.g.
In Linux: ffs 2
In Wiki example: ffs 3
接著來看在 lib/math/gcd.c 中提供的 gcd 實現演算法吧!
由於 gcd 的演算法上面已經列過,這邊就不重複說明。要進入解析之前,可以先來看看在程式碼中每當出現 b == 1
或 a == 1
時就會出現的 r & -r
是什麼。 r = a | b
就跟原本題目中程式碼的 u | v
的效果一樣,但是在特定條件下要回傳的 r & -r
就是很棒的寫法了(至少我這麼覺得), 由於 2 補數的運算邏輯,因此 r & -r
在 a && b
的情況下必為 a
和 b
之間的最大 2 的次方的公因數。
因此當 ((b >>= __ffs(b)) == 1) || ((a >>= __ffs(a)) == 1)
時就表示兩者間沒有除了 r & -r
以外的其他公因數了(類似題目中 shift
的作用),因此要回傳 r & -r
。
接著在理解 r
跟 __ffs
的作用後,就能直覺的了解當 a == b
時,回傳 a << __ffs(r)
的用意了(將最大 2 的次方的公因數補回去當前的最大公因數!)。
先來個簡單的,以下程式碼是利用 lib/math/gcd.c 中提供的 gcd 實作 Lowest common multiple (最小公倍數)。
可以看到這個想法很簡單,也很直覺。
接著來看到在 linux/drivers/iio/afe/iio-rescale.c 中的 gcd 應用。
節錄自 linux/drivers/iio/afe/iio-rescale.c 第 196 至 211 行。
先不管 iio 到底是什麼(我也不懂…),但是從註解還有程式碼中我們可以很清楚的看到,透過 gcd 能幫助找到兩特定值之間的最大公因數,進而防止某些值 overflow 。
從 naive 的版本中可以發現,整個程式的目的是為了紀錄 bitmap 裡每個 bitset 中的第一個 1 位於從最低位元算起的哪個位置,若 bitset 中沒有 1 則不紀錄,最後回傳有多少不為 0 的 bitset 。
在簡單了解程式碼的目的後,我們再來看 improved 的板本。
再根據題目的提示後可知, EXP6
的目的在於找出目前最低位元的 1 ,其他更高位元全部清 0 。
若達成這種效果可以利用 &
搭配反轉所有 bits 僅留最低位元的 1 來完成。
EXP6
bitset & -bitset
我暫時找不到該下什麼關鍵字才能更明確的找到應用實例。因此以下我先講我覺得這樣的程式碼能怎樣被應用。
能把一個 bitmap 轉換成用 out
、 pos
和將裡面的每筆資料進行去除 tailing zeros 之後的 bitmap 就能表示的方式,能壓縮原本 bitmap 所需的空間,若 bitmap 越稀疏,則壓縮效果越好。
以下例子直接沿用上面的程式碼及程式邏輯
e.g.
假設今天有一個 bitsize 為 4 的bitmapbm
bm:
0xFEDCBA9876543210
0xEFCDAB8967452301
0x1111000011110000
0x8888888888888888經過上面程式運算之後可得
out
和pos
pos = 4
out[0] =
out[1] =
out[2] =
out[3] =搭配經過對每個內容去除 tailing zeros後的
bm_rtz
bm_rtz:
0x0FEDCBA987654321
0xEFCDAB8967452301
0x0000111100001111
0x1111111111111111就能還原成原本的 bitmap 了!
若是今天 bitmap 中 set bits 是稀疏的,壓縮效果會更好。
bm:
0x0000000000010000
0x0010110102100000
0x0F0E000100600000
0x0000000100000000pos = 4
out[0] =
out[1] =
out[2] =
out[3] =搭配經過對每個內容去除 tailing zeros後的
bm_rtz
bm_rtz:
0x0000000000000001
0x0000000101101021
0x000000F0E0001006
0x0000000000000001
TODO: 延伸問題 2 3 4
結構圖解:
find
函式根據 hash 值在對應的 heads 中尋找對應的 key ,若有則回傳該 key 的 index ,若沒有則回傳 -1 。
fractionToDecimal
函式
由於程式很長,所以會分段講解。
abs
改進: branchless
改進: bitwise operation
改進: branchless
heads
pos == -1
的情況,先講解後續的程式碼。建立一個 node 並把當前的餘數存進 key 跟把目前的 i (當前重複處理幾次餘數)存進 index 。
接著把這個 node 放進 heads 中的前端,,並依據在 find
中對應的 hash 方式放進對應的 heads[hash] 中。
最後更新 decimal 和 remainder 。
MMM
list_add
EEE
&heads[remainder % size]
若在 heads 中找到 remainder 則表示已經進入重複循環了,接著從當前的餘數開始往回找重複幾個數字,最後輸出結果。
PPP
pos–
在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
TODO: 延伸問題 2
__alignof__
實作在進入程式碼前,我們先來看 __alignof__
是什麼。
The keyword
__alignof__
determines the alignment requirement of a function, object, or a type, or the minimum alignment usually required by a type. –__alignof__
逐步拆解這個 marco 可以發現它的本質是兩個 (char *)
(pointer of char) 相減的結果,並藉此取得兩個地址間相差多少 bytes 。但是對 c 不熟的人看到這裡可能會開始冒出一堆疑惑(就像是我!),(struct { char c; t _h; } *)0
是什麼寫法?為什麼 struct 中還要多一個 char c
?
(struct { char c; t _h; } *)0
是什麼?首先 (struct { char c; t _h; } *)
只是表示內含 { char c; t _h; }
的 pointer of struct ,本質上他還是某種型態的 pointer ,所以像是 (char *)
或是 (void *)
都是合法的。回到正題,(struct { char c; t _h; } *)0
是什麼?上網搜尋會找到很多有關這個問題的討論。
Is (int *)0 a null pointer? -stackoverflow
What does (char*) 0 mean? -stackoverflow
What does (char *)0 mean in c? -stackoverflow
what is the meaning of (char*)1? -stackoverflow
相信概念不夠清楚的人看完這些討論後,應該會更一頭霧水,有人說
(char*) 0 Is not a null character, but a pointer to a character at address 0.
而在其他篇中有人認為
In C (char *) 0 is treated in a special way - it is guaranteed to produce a null-pointer value of type char *.
還有人補充
In both C and C++, (int *)0 is a constant expression whose value is a null pointer. It is not, however, a null pointer constant.
因此還是來找找 C99 standard 吧!
乍看之下,(char *)0
這種表示法挺像是 cast
的,因此來看看 C99 standard 中 §6.5.4 (Cast operators) 的說明,可以在第 3 點中看到以下說明:
Conversions that involve pointers, other than where permitted by the constraints of 6.5.16.1, shall be specified by means of an explicit cast.
由於這是一個有關指標的轉換但不確定是不是 §6.5.16.1 中規定的例外狀況,因此我們再跳到 §6.5.16.1 看有關指標轉型的說明。
而在 §6.5.16.1 中規定有關 pointer assignment 的規定,因為與這個無關就不放上來了。
看到這裡似乎可以把 (char *)0
當成把 0 轉換成 pointer of char 。這樣第一個回復的內容似乎是正確的?接著我們再來看看 §6.3.2.3 第 3 點中的說明。
根據 C99 standard 中 §6.3.2.3 第 3 點中的說明。
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
來舉個例子說明吧!
e.g
int *p = 0; //null pointer constant
int *p = (void *)0; //null pointer constant
int *p = (int *)0; //null pointer
所以 (struct { char c; t _h; } *)0
是一種 null pointer 。
題外話,那 (int *)1
是什麼呢?按照以上的說明可知, (int *)1
是把 1 cast 成 pointer of int ,所以 (int *)0
跟 (int *)1
是不一樣的。
char c
?char c
會依據後續的資料來決定要 padding 多少。e.g.
後面接char
,因為一樣是char
所以不用 padding。
後面接int
,以int
(4 bytes) 為基準,因此 padding 3 bytes 。
後面接long
,以long
(8 bytes) 為基準,因此 padding 7 bytes 。
利用這個特性就可以從 char
經過 padding 之後的地址,知道後面我們好奇的那個資料型態需要用多少 bytes 來 align 。
M
_h
X
0
在 Linux 核心原始程式碼中找出 alignof 的使用案例 2 則,並針對其場景進行解說
在 linux/drivers/usb/gadget/u_f.h 可以看到 __alignof__
被拿來計算與下一個 groupname
(groupname##__next
) 間的 offset 。
在 Linux 核心源使程式碼找出 ALIGN, ALIGN_DOWN, ALIGN_UP 等巨集,探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)。思索能否對 Linux 核心提交貢獻,儘量共用相同的巨集
在 linux/include/uapi/linux/const.h 中可以找到對齊相關的程式碼
以下程式碼節錄自 const.h 第 31,32 行
__ALIGN_KERNEL_MASK
__ALIGN_KERNEL
__ALIGN_KERNEL_MASK
的基礎上,對特定長度的數字(通常是 ) 進行向上對齊接著在 linux/include/linux/align.h 中可以找到向上對齊/向下對齊的程式碼。
以下程式碼節錄自 align.h 第 7 至 9 行
ALIGN
__ALIGN_KERNEL
中就是向上對齊了,所以他就是向上對齊。ALIGN_DOWN
- ((a) - 1)
,達到把 a - 1
以下的位元全部 clear 並且不 +1
,完成向下對齊。直接看 length
會不知道他要做什麼,因此我們先從&"FizzBuzz%u"[(9 >> div5) >> (KK3)]
說起。當 div5 等於 1 時(輪到 5 的倍數,先不討論 15 的倍數),要輸出 Buzz
,而此時 &"FizzBuzz%u"[(9 >> div5)]
會等於 Buzz%u
,另外 strncpy
會把 src
中的字串從前到後根據 length
複製到 dest
。
因此可知 KK3
的作用是在 3 的倍數(包含 15 的倍數)時,讓字串變成 "FizzBuzz%u"
,接著要讓 變成 至少要右移 4 。
KK3
div3 << 2
接著來看 length
在 3 的倍數時(字串此時是 FizzBuzz%u
),要等於 4 ;在 5 的倍數時(字串此時是 Buzz%u
),要等於 4 ;最後在 15 的倍數時(字串此時是 FizzBuzz%u
)要等於 8 。
KK1
div3
KK2
div5