contributed by < jim12312321
>
解析︰
先觀察程式碼,~0UL
等於 0xFFFFFFFFFFFFFFFF ,有出現 shift
,中間用 AND
相連,因此我們可以合理猜測本題中應該是想透過將 ~0UL
經過特定的 shift
後再透過 AND
將不在 [h,l]
中的 1 消除。
e.g.
要產生GENMASK(6,4)
,要先產生 0x000000000000007F (~0UL
右移 57 個 bits) 和 0xFFFFFFFFFFFFFFF0 (~0UL
左移 4 個 bits),再藉由AND
產生 0x0000000000000070 。
由上述例子進行延伸討論,我們可以知道若要透過 ~0UL
產生 0x000000000000007F 需要右移,反之要產生 0xFFFFFFFFFFFFFFF0 則需要左移。
再與程式碼比較後可知,(~0UL) >> (LEFT)
僅有右移,(~0UL) >> (l) << (RIGHT)
包含左移操作。因此可知若對應上述例子,(~0UL) >> (LEFT)
目的在於產生 0x000000000000007F ,(~0UL) >> (l) << (RIGHT)
目的在於產生 0xFFFFFFFFFFFFFFF0 ,不過由於在 (~0UL) >> (l) << (RIGHT)
中還含有右移行為,因此產生出來的 bitmask 應該為 0x0FFFFFFFFFFFFFF0 。
最後可以發現右移操作中的 57 來自於 64-1-h ,而左移操作中的 4 來自於 l。
LEFT
63 - h
RIGHT
l
比較 Linux 核心 GENMASK 巨集的實作,闡述其額外的考量
在 linux kernel v5.16.12 中的 isst.h 中可以發現 GENMASK 的程式碼
在這段程式碼中可以發現與本題中的 GENMASK 幾乎一致,僅差在 (~0UL) << (l)
的操作中沒有先左移 l ,但這對於答案是沒有差別的。
另外在 linux kernel v5.16.12 中的 bits.h 中也可以發現 GENMASK 的程式碼。
接著我們分別來看 GENMASK_INPUT_CHECK(h, l)
和 __GENMASK(h, l)
搭配 BUILD_BUG_ON_ZERO 的說明
我們可知在 BUILD_BUG_ON_ZERO(e)
中當 e 這個情況為 0 時沒事,而不為 0 時 (condition is true) ,印出錯誤訊息。
接著來看 __builtin_choose_expr
的說明
Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2)
You can use the built-in function __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.
再來看 __is_constexpr
的說明
最後透過整理上述三個資訊可知,GENMASK_INPUT_CHECK(h, l)
的目的在於確認 h > l ,若是輸出 0;若否 (h <= l) ,則印出錯誤訊息。
__GENMASK
我們一樣來舉個例子說明(不舉例給自己看我真的腦袋卡住)
e.g.
要產生GENMASK(6,4)
,要先產生 0x000000000000007F (~0UL
右移 57 個 bits) 和 0xFFFFFFFFFFFFFFF0 (~0UL
左移 4 個 bits),再藉由AND
產生 0x0000000000000070 。
這跟上面舉的例子是完全相同的,而觀察程式碼可以發現, ~UL(0) >> (BITS_PER_LONG - 1 - (h))
的作用等同 (~0UL) >> (63 - h)
,一樣是為了左移 4 個 bits 產生 0x000000000000007F 。
接著來看 (((~UL(0)) - (UL(1) << (l)) + 1)
想做什麼。
(~UL(0))
產生 0xFFFFFFFFFFFFFFFF(~UL(0)) - (UL(1) << (l))
產生 0xFFFFFFFFFFFFFFEF((~UL(0)) - (UL(1) << (l)) + 1)
產生 0xFFFFFFFFFFFFFFF0ya! 結果和 ((~0UL) << (l))
是等價的!只是這裡的想法是先目標位置 (l) 的 bit 設為 0 ,再透過 +1
產生我們想要的 bitmask 。(雖然我覺得想法上沒有 ((~0UL) << (l))
來的簡潔和直覺就是了)。
總結來說在 linux kernel 中的 GENMASK
並沒有什麼很特別的技術,僅是在 linux kernel v5.16.12 理的 bits.h 中的 GENMASK
多了要求 h 恆大於 l 的限制罷了。
舉出 Linux 核心原始程式碼中二處 GENMASK 巨集和 include/linux/bitfield.h 的應用案例
在 linux 中有不少 GENMASK 巨集和 include/linux/bitfield.h 的應用案例(請看這裡)。大部分跟裝置驅動程式或是網路挺有關係的,礙於能力有限,我會盡量理解後留下心得。
device driver 請稱呼全名「裝置驅動程式」,不要簡稱「驅動」,不然日後涉及到編譯器和資料庫,都有對應的 compiler driver 和 database driver,必然會混淆。
本段程式碼可以在 linux/sound/soc/intel/keembay/kmb_platform.h 的第 64 到 66 行中找到。
可以猜測這是用來設立 flag 用以判斷該發送數據或是接收數據。
接著繼續追蹤 TX_INT_FLAG
和 RX_INT_FLAG
可發現當 stream 狀態為播放時,將 flag 設定為傳輸模式;否則將 flag 設定為接收模式。
本段程式碼可以在 linux/net/dsa/tag_ar9331.c 的第 13 到 15 行中找到。
接著追蹤 AR9331_HDR_VERSION_MASK
,可以在同一個檔案中的第 36 行找到他。(下面程式碼中的第 10 行)
再來看看 FIELD_PREP 的說明。
結合上述的內容我們可以推斷在 linux/net/dsa/tag_ar9331.c 中的 GENMASK
目的是為了結合 FIELD_PREP
將特定的值左移到特定位置。
在進入程式碼解析之前,先來複習什麼是記憶體向上對齊和向下對齊,在⟨你所不知道的 C 語言:記憶體管理、對齊及硬體特性⟩ 中的 data alignment 中有提到,記憶體對齊就是讓記憶體內的資料落在 4 個 bytes 或是 8 個 btyes 的倍數位置(當然也可以根據需求設置對齊任意位置) ,方便 cpu 抓取資料。
以下例子皆以對齊 4 個 bytes 為例。
address (after align up) :
address (before align up) :
address (after align up) :
address (before align up) :
看完說明之後就可以來解析程式碼了。
由於本題要將結構體中第一個成員的地址向下對齊 4 個 bytes ,又因為 forward declaration
的關係,所以在這個回傳式中要加入對 (struct foo *) 的操作。
EXP1
(struct foo *)(v & ~3)
在 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
,完成向下對齊。觀察程式碼行為可以得知,這段程式碼正在逐步將位元交換,一開始先交換前 4 個 bits 和後 4 個 bits,接著將交換完的 4 個 bits 內的 bits 兩兩互換,最後在將兩個 bits 互換。(有點 mergesort 的感覺)
EXP2
(x & 0x33) << 2
EXP3
(x & 0x55) << 1
在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景
在 linux/lib/bitrev.c 中可以發現由於反轉 8 bits 僅有 256 種可能,因此開發者直接把 8 bits 反轉的查詢表內建在裡面了。
節錄自 bitrev.c 中第 11 至 44 行
而在 linux/include/linux/bitrev.h 中則可以看到基於 byte_rev_table
延伸出來的 16 位元、 32 位元版本。
另外在 bitrev.h 中有提供用於 reverse 編譯時常數的版本,作法就根本題是幾乎一模一樣了。
因此在 linux 中真正在使用 bit reverse 時也是要先考慮輸入值是否為編譯時常數。
在很多裝置驅動程式相關的程式碼中都能看到 bit reverse 的身影,以下程式碼雖不能完整解釋,就當放上來作為實際應用情境參考。
在 linux/drivers/media/usb/cx231xx/cx231xx-input.c 的 get_key_isdbt
函式中可以看到以下程式碼。
又或者在 linux/drivers/media/rc/img-ir/img-ir-nec.c 中的 img_ir_nec_scancode
函式可以看到解析 raw code
to scan code
時用到 bitrev8
。
foreach 的概念很簡單,就是依序提取出特定 array 內的所有資料。
先來看程式碼。
無論是 foreach_int
或 foreach_ptr
目標都是為了走訪 array 中所有資料,對應到 c 中 for 的虛擬碼就會是:
為求精簡,題目中在一行內就要更新 element (題目中的 i
) 與 index (題目中的 _foreach_i
) 因此使用 ++Variable
的技巧。
EXP4
++_foreach_i
EXP5
++_foreach_i
在 Linux 核心原始程式碼找出類似技巧的實作並解說其應用場景
在 drivers/net/ethernet/mellanox/mlxfw/mlxfw_mfa2_tlv_multi.h 中可以找到類似的實作技巧。
這邊的作法跟 list.h 中的 list_for_each
之類的實作挺類似的,就不特別解說。
這類型的技巧可以用來檢查特定變數/結構體有沒有符合特定條件。在同個目錄中的 mlxfw_mfa2.c 可以看到應用的場景。
EXP6
dvs << shift
EXP7
dvd -= dvs << shift
本題程式的目的在於將可連成一條線的點( 相同者,在本題中 x
與 y
都有先除以 gcd(x,y)
,以確保 hash 值相同),存在 hash table 同個 hash 值的 linked list 中。
所以 EXP8
的目的在於判斷傳進來的 (p1,p2)
與原先 list 中的 (p1,p2)
是否一致。
EXP8
p1 == pn->p1
or p2 == pn->p2
而 EXP9
的目的就在於將新的節點放進 heads
中對應 hash 的位置。
EXP9
list_add(&pn->link, &heads[hash])
考慮題目一開始的要求
針對給定的 32 位元無號數,計算扣除開頭的 0,最少需要多少位元才可儲存
相當於對特定的數字進行 的操作再取 floor 。因此這題便是對前述概念的實作。
觀察題目後可以發現有類似的模組重複發生,如下所示。
每次對半檢查,判斷對半後較高位的那組中是否有值。若有則記錄判斷了幾個 bits ,反覆檢查直至全部檢查過。
到 EXP10
之前的程式碼檢查到 4 個 bits ,因此可知 EXP10
要檢查剩下未確定的 4 個 bits 中,較高位的 2 個 bits 是否為 0。
EXP10
(v > 0x3) << 1
理論上還要再進行一次針對剩下 2 個 bits 的操作, ret
的結果才會是最終答案,如下所示。
題目中顯然是希望最後這三道指令能濃縮成一道,首先 v >>= m
,與最後回傳值無關,不理他。剩下 m = (v > 0x1)
跟 ret |= m
。不過由於最初就是考慮到每次位移的 bits 都不會重複,因此在這個情況下 |=
等價於 +=
。
EXP11
ret |= v > 1
or ret += v > 1
以下解說只挑有填空的位置講解。
根據二元搜尋樹的特性,若在當前節點無法匹配到指定值且當前節點不為葉節點,匹配值較當前值大則向右搜尋,反之向左。
EXP12
p = &(*p)->left
EXP13
p = &(*p)->right
這段在處理欲移除節點擁有同時擁有左右子樹的情況,此時應拿全部小於該數值中最接近該數值的值取代(左子樹中最右邊的值),或全部大於該數值中最接近該數值的值取代(右子樹中最左邊的值),而在本題中使用的是後者。
EXP14
&(*r)->left
上面有解釋過何謂向上對齊,這邊簡單複習。
以下例子皆以對齊 4 個 bytes 為例。
address (after align up) :
address (before align up) :
接著來看程式碼。
因此本題希望將 x 向上對齊到 4 的倍數的位置上 (e.g. ) ,並且將最低 4 個 bits 清空。
MMM
1
NNN
MAX_ALIGNMENT - 1
用來進行判斷進入 ((RRR) / (__d))
或 ((SSS) / (__d))
的三個條件,主要目的便是拿來判別運算結果有沒有可能為負,若不可能就進 ((RRR) / (__d))
否則進 ((SSS) / (__d))
。
三個條件中須注意的是 ((typeof(x)) -1)
這是用以將 -1 cast 成 typeof(x)
,假設 x 的 type 為 int ,則 ((typeof(x)) -1) > 0
不成立,若 x 的 type 為 unsigned int ,則 ((typeof(x)) -1) > 0
成立(之所以要檢查是否為 unsigned ,是因為跟 unsigned 運算的 signed 數值也會變 unsigned ,因此恆大於等於 0)。
考慮到在 c 中除法會無條件捨去,因此對於除完後餘數大於等於 0.5 的數值要補齊這一半,利用加上這一半的值讓他進位;而對於小於 0.5 的情況,有沒有補都不會造成進位,因此補上去也沒關係。而在負數的除法中情況相反,對於除完後餘數大於等於 0.5 的數值要補齊這一半,利用減去這一半的值讓他進位。
RRR
(__x) + ((__d) >> 1)
SSS
(__x) - ((__d) >> 1)
fls(x)
的目的在於找出最高位的 set bit 的位置。
fls(1) == 0
,fls(2) == 1
,fls(0x8000 0000 0000 0000) == 63
一直卡住沒什麼想法,在參考 kdnvt 同學的解釋之後有了一點頭緒。
用 2 進位的邏輯 可以被表示為 ,其中 () 表示 或 0 ,因此我們可以從 找回 並把結果記錄下來 。
為了決定 中的 屬於 或 0 ,利用以下規則︰
If ,
Else,
對應的程式碼
並且為了每次都去計算 ,在每一次計算的時候,我們僅紀錄 ,並用 的方式更新 。(其中 )
接著要從最大的 n 開始往下找,也就是 m = 1UL << (fls(x) & ~1UL)
這行的作用,並令 。然後我們用兩個新變數 和 分別儲存 中的兩個部份。
(本題的
y
)
(本題的m
)
最後 和 會利用以下方式更新。
.
If , . Else
XXX
y >>= 1
YYY
x -= b
ZZZ
m >>= 2