contributed by < hsuedw
>
在 Linux 核心原始程式碼,include/linux/bitfield.h 提及一個巨集 GENMASK,其作用是依據給定的範圍,產生連續的 bitmask,例如:
GENMASK(6, 4)
產生 011100002GENMASK(39, 21)
產生 0x000000ffffe00000 (64 位元)已知我們使用的微處理器架構為 64 位元,且 unsigned long
為 8 位元組寬度 (符合 LP64 資料模型),以下是可能的 GENMASK
巨集的定義:
LEFT = ?
RIGHT = ?
63 - h
l
&
(bitwise and operator) 為中心,分為左右兩部分討論。以 GENMASK(39, 21)
為例進行討論。((~0UL) >> (l) << (l))
bit 0
到 bit l - 1
(l
等於 21) 須為 0。所以,必須左移 l
bits,使得 bit 0
到 bit l - 1
皆為 0。((~0UL) >> (64 - h - 1))
63 - h
個 bit。
>>
操作的是無號數 (unsigned integer),所以每向右移一個 bit,就會在 most significant bit 補一個 0。
6.5.7 Bitwise shift operators
The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.
An arithmetic left shift is equivalent to a logical left shift
(as far as GCC is concerned).For right shifts, if the type is signed, then we use an arithmetic shift;
if the type is unsigned then we use a logical.
GENMASK(h, l)
巨集基本上要在 h >= l
的前提下才合理。否則,會產生全部都是 0 的 bit mask。Linux 核心中實作的 GENMASK
使用 BUILD_BUG_ON_ZERO
巨集與 __builtin_choose_expr
GCC built-in function 檢查 h
與 l
是否合理。若發現 l > h
則強制編譯器發生編譯錯誤。
GENMASK
巨集的實作出現在 include/linux/bits.h。相關程式碼節錄如下。
BUILD_BUG_ON_ZERO
實作於 include/linux/build_bug.h。若 e
為非零值,則強制編譯器產生編譯錯誤。基本上就是利用 bit-field 的寬度為負值強制使編譯器發生錯誤。
6.7.2.1 Structure and union specifiers
The expression that specifies the width of a bit-field shall be an integer constant expression with a nonnegative value that does not exceed the width of an object of the type that would be specified were the colon and expression omitted. If the value is zero, the declaration shall have no declarator.
__builtin_choose_expr(const_exp, exp1, exp2)
是 GCC 的 built-in function。若 const_exp
為 true,則回傳 exp1
的計算結果(型別與 exp1
一致)。否則,回傳 exp2
的計算結果(型別與 exp2
一致)。
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.
This built-in function is analogous to the ‘? :’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side effects.
This built-in function can return an lvalue if the chosen argument is an lvalue.
If exp1 is returned, the return type is the same as exp1’s type. Similarly, if exp2 is returned, its return type is the same as exp2.
__is_constexpr 為實作在 include/linux/const.h
中的巨集。主要是用來檢查輸入參數是否為 constant expression。
6.6 Constant expressions
A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.
參考資料:
bit-field
GENMASK
巨集和 include/linux/bitfield.h 的應用案例考慮以下程式碼:
函式 fo_fd 接受指定的地址 v,填入一個 fd 結構體,並確保第一個成員的地址得以依據〈你所不知道的 C 語言:記憶體管理、對齊及硬體特性〉描述,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 align_down)。其中 struct foo; 運用〈你所不知道的 C 語言:指標篇〉提及的 forward declaration 技巧,可在實作階段才提供具體 struct foo 的定義。
請補完,使其行為符合預期。作答規範:
EXP1
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP1
不得使用 %
(modulo) 運算子v | 1
EXP1
= (struct foo *)(v & ~3)
根據題意,要將記憶體位址 v
對齊到小於或等於 v
且能夠被 4 整除的位址上(align_down)。
A value at or before value that is a multiple of alignment.
若以二進位表示一個數值,且此數值會被 4 整除,則其 bit 0 與 bit 1 皆為 0。所以,基本上,可實作為 v & ~3
。
不過,因為 to_fd()
是回傳一個 struct fd
型態的 object。所以,v
向下對齊到可被 4 整除的位址後,須強制轉型為指向 struct foo
object 的指標。所以,EXP1
應寫為 (struct foo *)(v & ~3)
考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 LeetCode 190. Reverse Bits 的描述。
請補完,使其行為符合預期。作答規範:
EXP2
和 EXP3
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP2
= (x & 0x33) << 2
EXP3
= (x & 0x55) << 1
延伸 〈你所不知道的 C 語言:前置處理器應用篇〉,我們嘗試將 foreach 巨集 予以擴充,使得支援以下寫法:
預期輸出如下:
對應的巨集定義:
請補完,使其行為符合預期。作答規範:
EXP4
和 EXP5
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP4
= ++_foreach_i
EXP5
= ++_foreach_i
foreach_int
的說明如下:
__VA_ARGS__
是屬於前置處理器 (preporcessor) 的功能。主要是用來實作不確定個數的參數。以 foreach_int
為例,經前置處理器展開後,程式碼如下所示。
C99 standard
6.10.3 Macro replacement
The identifier__VA_ARGS__
shall occur only in the replacement-list of a function-like
macro that uses the ellipsis notation in the parameters.
(int[]){0, -1, 100, 0, -99}
表示一個長度為 5 的整數陣列,其各元素值分別為 0, -1, 100, 0, -99。
((int[]){0, -1, 100, 0, -99})[0])
就是讀取該整數陣列索引值為 0 的那個元素,也就是第一個 0。
((i) = ((int[]){0, -1, 100, 0, -99})[0])
就是將整數陣列索引值為 0 的那個元素的值指派給變數 i
。所以,到目前為止,變數 i
的值為 0。
到目前為止 unsigned _foreach_i = (((i) = ((int[]){0, -1, 100, 0, -99})[0]), 0)
這段程式碼可以概念上的看作為 unsigned _foreach_i = (0, 0)
。第一個 0 是因為變數 i
被指派為 0 (assignment expression)。第二個 0 是題目程式碼中原本的數字常數。基本上,這個看其來很複雜的程式碼就是做兩件事。
i
,所以變數 i
的值為 0。_foreach_i
初始化為 0 (就是 unsigned _foreach_i = (0, 0)
裡的第二個 0)。這與 ,
和 =
兩個運算子的優先順序與 ,
運算子的結合性(associativity)有關。C Operator Precedence
Difference between "int i=1,2,3" and "int i=(1,2,3)" - variable declaration with comma operator [duplicate]
C99 standard, 6.5.16 Assignment operators
An assignment operator stores a value in the object designated by the left operand. An
assignment expression has the value of the left operand after the assignment, but is not
an lvalue. The type of an assignment expression is the type of the left operand unless the
left operand has qualified type, in which case it is the unqualified version of the type of
the left operand. The side effect of updating the stored value of the left operand shall
occur between the previous and the next sequence point.
接下來,執行 printf()
。因為變數 i
已經被指派為整數陣列的第一個元素值,所以會印出 i is 0
。
在 for
迴圈進入下一回合的迭代之前,會先檢查變數 _foreach_i
的值是否小於整數陣列的長度。
若變數 _foreach_i
的值小於整數陣列的長度,則會執行 (i) = ((int[]){0, -1, 100, 0, -99, 0})[++_foreach_i]
。因為此時變數 _foreach_i
為 0,也就是指向整數陣列的第一個元素,所以要用 prefix increment operator 讓變數 _foreach_i
先遞增 1,也就是指向第二個元素。然後再將整數陣列的第二個元素值指派給變數 i
。所以,變數 i
就會是 -1。然後,又再一次執行 printf()
把 i is -1
印出來。
以此類推,直到把整個整數陣列的所有元素都走訪一遍為止。
foreach_ptr
的實作手法與 foreach_int
如出一轍。
針對 LeetCode 29. Divide Two Integers,以下是可能的實作:
請補完,使其行為符合預期。作答規範:
EXP6
和 EXP7
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP6
= dvs << shift
EXP7
= dvd -= dvs << shift
為了說明方便,將程式碼補齊後,如下:
dividend
)與除數(divisor
)間,一為正,另一為負。則最後的答案為負。所以變數 signal
會被指派為 -1
。且變數 dvd
會被指派為變數 dividend
的絕對值,變數 dvs
會被指派為變數 divisor
的絕對值。接下來就以 dvd
與 dvs
做除法運算。res
的值就是除法運算中的商數,變數 dvd
的值就是餘數。res
(型態為 unsigned int
)的值大於或等於有號整數的最大值(INT_MAX
),則回傳 INT_MAX
。針對 LeetCode 149. Max Points on a Line,以下是可能的實作:
請補完,使其行為符合預期。作答規範:
EXP8
和 EXP9
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP8
不該出現小括號 (即 (
和 )
)EXP9
為包含 list_
開頭巨集的單一敘述EXP8
= pn->p1 == p1
EXP9
= list_add(&pn->link, &heads[hash])
考慮 ilog32
函式可針對給定的 32 位元無號數,計算扣除開頭的 0,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作:
請補完,使其行為符合預期。作答規範:
EXP10
和 EXP11
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息EXP11
不該出現小括號 (即 ( 和 ))EXP10
和 EXP11
皆包含 > 運算子EXP10
: (v > 0x3) << 1
EXP11
: ret += v > 1
v
大於 0xFFFFU
表示第一個為 1 的位元落在 bit 31~16 之間。否則,落在 bit 15~0 之。
v > 0xFFFFU
成立,將 24 指派給 m
,也就是 m
的值是 16。接著 v
往左位移 16 個位元(第 5 行)。也就是第一個為 1 的位元往左位移 16 個位元。然後把 m
累加到 ret
中。v > 0xFFFFU
不成立,將 0 指派給 m
。 v
與 ret
的值維持不變。EXP10
應該實作為 (v > 0x3) << 1
。ilog
考慮以下 C++ 二元搜尋樹的程式:
函式 remove_data
嘗試將指定的資料,自給定的二元搜尋樹移除,但原本的 remove_data
過於冗長,於是我們可善用 indirect pointer 改寫為以下:
請補完,使其行為符合預期。作答規範:
EXP12
, EXP13
和 EXP14
應以最簡潔的 C99 形式撰寫,符合作業一排版規範,且不該觸發編譯器警告訊息,並善用 indirect pointer 的技巧來撰寫
EXP12
, EXP13
和 EXP14
皆不該出現 ;
EXP12
: p = &(*p)->left
EXP13
: p = &(*p)->right
EXP14
: &(*r)->left
remove_data()
的作用是要將 t
所指向的二元搜尋樹中,具有與變數 d
的值相同的節點刪除。d
的值相同的節點,第 15~21 行就是在做這件事。根據二元搜尋樹的定義,當要尋找的目標值小於當前節點的值時,就要往左子樹移動。反之,往右子樹移動。如下圖所示,用指標的指標 p
走訪二元搜尋樹。*p
不為 NULL
。第二種就是想要刪除的節點不在二元搜尋樹中,也就是說 *p
為 NULL
。remove_data()
的工作。q
指向即將被刪除的節點。指標 p
指向父節點中指向即將被刪除的節點的指標。q
所指向之節點(也就是即將被刪除的節點)的左子樹為空,則 p
指向該節點的右子樹(此右子樹可能為空,也可能不為空)。然後刪除 q
所指向之節點。q
所指向之節點(也就是即將被刪除的節點)的右子樹為空,則 p
指向該節點的左子樹。然後刪除 q
所指向之節點。q
所指向之節點(也就是即將被刪除的節點)的左右子樹皆不為空,則先找到指標 q
所指節點的 inorder successor,再將 inorder successor 的資料指派給指標 q
所指的節點。因為二元搜尋樹的 inorder traversal 的結果就是由小到大的排列結果,所以用 inorder success 的值取代指標 q
所指的節點 (就是即將被刪除的節點) 的資料,再將 inorder successor 刪除,可保持二元收尋樹的特性。刪除流程如下圖所示。考慮以下進行記憶體地址對齊 (alignment) 的巨集:
作用是向上對齊 (roundup),請補完程式碼,使其行為符合預期。作答規範:
MMM
是常數NNN
是表示式MMM
和 NNN
皆不得出現小括號 (即 ( 和 ))MMM
: 1
NNN
: MAX_ALIGNMENT - 1
ROUND_DOWN
巨集x
的向上調整到 16 的整數倍。
x
向上對齊為 16 的倍數後,必大於或等於 x
目前的值。所以,對 x
加上 15 後,再與 ~15 做 bitwise AND,把 bit 0~3 清為 0。
x
的值介於 1~16 為例,x
加上 MAX_ALIGNMENT - 1
(即 15)之後,就會介於 16~31 之間。然後再與 ~(MAX_ALIGNMENT - 1)
(即 ~15)做 bitwise AND 就可保留 bit 4 的 1,並且把 bit 0~3 清為 0。MMM
與 NNN
分別實作為 1
與 MAX_ALIGNMENT - 1
。考慮以下巨集可得到二個表示式進行整數除法操作時,最接近的整數值:
注意: 當除數 (即 divisor) 為負值時,行為沒有定義。
參考執行結果:
DIVIDE_ROUND_CLOSEST(7, 3) = 2
DIVIDE_ROUND_CLOSEST(6, 3) = 2
DIVIDE_ROUND_CLOSEST(5, 3) = 2
作答規範:
RRR
和 SSS
為表示式,且都包含 (__x)
和 (__d)
(注意小括號)RRR
和 SSS
限用 +
, -
, >>
, <<
這幾個運算子,可搭配小括號,並以符合 C99 的最精簡形式撰寫RRR
和 SSS
出現的順序 (從左到右),必定是 __x
先於 __d
RRR
= (__x) + ((__d) >> 1)
SSS
= (__x) - ((__d) >> 1)
就我的理解,這一題應該是要求 x
除以 divisor
後,取四捨五入。為了說明方便,將成碼補齊後,如下所示。
以變數 x
為例,第 5 行的 ((typeof(x)) -1) > 0
判斷式作用檢查 x
是否為無號數。若 x
為無號數,則判斷式成立。反之,若 x
是否為有號數,則判斷式不成立。同樣的想法可套用在 ((typeof(divisor)) -1) > 0
上。
參考資料:
6.7 Referring to a Type with typeof
你所不知道的C語言:技巧篇, 善用 GNU extension 的 typeof
解讀計算機編碼
第 6 行。(((__x) > 0) == ((__d) > 0))
的功用是在判斷是否同時為正整數,同時為負整數,或者同時為 0。也就是執行除法運算後,結果是否大於或等於 0。若是,則判斷式成立。否則,判斷式不成立。
綜合以上所述,當下列兩個條件同時成立時,就會執行第 8 行的運算式。否則,執行第 7 行的運算式。
x
與 divisor
同時為有號數。x
與 divisor
一為正整數,另一為負整數,或者不同時為 0。C99 standard 對整數除法的描述如下。所以,C 語言的除只取商數,小數部分則是無條件捨去。
6.5.5 Multiplicative operators
The result of the / operator is the quotient from the division of the first operand by the
second; the result of the % operator is the remainder. In both operations, if the value of
the second operand is zero, the behavior is undefined.
When integers are divided, the result of the / operator is the algebraic quotient with any
fractional part discarded.88) If the quotient a/b is representable, the expression
(a/b)*b + a%b shall equal a.88) This is often called ‘‘truncation toward zero’’
先假設 x
與 divisor
不同時為有號數,或者執行除法算後結果大於或等於 0。兩數相除,
x
加上 divisor / 2
再與 divisor
相除。就可以達到五入的效果。x
加上 divisor / 2
再與 divisor
相除。就可以達到四捨的效果。也就是在 x
加上 divisor / 2
前,效果一樣。RRR
應寫為 (__x) + ((__d) >> 1)
。x
與 divisor
同時為有號數,且執行除法算後結果小於 0。則 SSS
應寫為 (__x) - ((__d) >> 1)
。考慮以下計算整數開平方根的實作:
i_sqrt
函式的作用等同於 floor(sqrt(x))
作答規範:
XXX
, YYY
和 ZZZ
都是敘述,不得呼叫任何函式,且不包含 ; 字元=
, +=
, -=
, >>=
, <<=
等運算子,且不得出現小括號 (即 ( 和 ))XXX
= y >>= 1
YYY
= x -= b
ZZZ
= m >>= 2
為了說明方便,將程式碼補齊後,還原如下。
第 1~28 行。 fls()
的功用是在計算其輸入參數 word
在二進位表示法中,最高位個為 1 的位元的位置。
fls(0100110100102) = 1010
參考資料
雖然教授問答的時候是從二進位的角度去想,但我第一次看到類似的原理其實是從十進位的直式開方法。不過直式開方法所有進位都適用,所以從十進位的想法轉成二進位也沒有很難。
直式開方法是把要開方的數拆成二的冪相加(如果是十進位就是十的冪),就可以拆成以下的形式:
上面的 為 bits pattern 。
為最高位的 1 ,
寫成數學式如下:
最後的形式為 的相加,而這每一項其實就是程式中每一輪迴圈
要判斷並可能減掉的
b
。以下大致講解原理:
迴圈從 0 開始。
為每個第 個迴圈一開始的y
值。
為每個第 個迴圈一開始的m
值。
為 。
因為確定 是最高位不為零,所以
而每經過一輪 會右移兩位,他的平方根 就等於右移一位:
如果此時 的結果小於當前的 ,代表 的展開式中 這項不為零,也就是 。將 右移一位後加上 ,就會變成
而如果此時 的結果大於當前的 x ,代表 的展開式中 這項等於零,也就是 。將 右移一位後就會變成
照同樣的原理推下去可以發現, 其實就等於 。因為不是所有的 ,所以我用另一個變數 來表達原式中的部份, ,如果 也就是 ,則 會等於 而且下圖的 會等於 :
每一輪迴圈就是判斷 有沒有 並更新 ,
到了最後的 , 會先往右移變成 ,在加上最後的 ,變成 ,就是我們要求的平方根。
lib/math/int_sqrt.c
arch/xtensa/include/asm/bitops.h