contributed by < YiChainLin
>
實驗的 gcc 編譯器版本
在 Linux 核心原始程式碼,include/linux/bitfield.h 提及一個巨集 GENMASK
,其作用是依據給定的範圍,產生連續的 bitmask,例如:
已知我們使用的微處理器架構為 64 位元,且 unsigned long 為 8 位元組寬度 (符合 LP64 資料模型),以下是可能的 GENMASK 巨集的定義:
1
遮罩bitmask
為:在 ((~0UL) >> (LEFT))
敘述中要能產生最高位前面的 0,所以透過邏輯移位補 0 的特性中,要向右 shift 64 - (h + 1),64 位元是來自於題目的架構,如果是 32 位元的架構則 為 31 - h
LEFT = 63 - h
所以對應這個敘述可以產生這樣的結果:
再來就是要將尾段清零,透過與 0 的 &
計算去除,所以透過向左移位補 0 的特性,可以補 l
個數的 0
所以在 ((~0UL) >> (l) << (RIGHT))
的敘述中,透過右移補 0 的方式產生,先進行向右移位 l
次,再左移 l
次
RIGHT = l
最後再將 high 的位移結果加入進行 &
運算
有一點比較不懂的是在 (~0UL) >> (l)
這段敘述中,為什麼要右移,如果只是想要在尾端補 0 的時候,那只要左移 l
次就可以了
也就是在右半部分只要 (~0UL) << (l)
能達到一樣的效果
考慮以下程式碼
函式 fo_fd
接受指定的地址 v
,填入一個 fd
結構體,並確保第一個成員的地址,對 4 個位元組進行向下對齊 (即 C++ Boost 描述的 align_down
)。
v
的值處理能被 4 整除的數字,所以在二進位元的表示中,在最後兩位的 bit 要等於 0&
運算子可將 v 的最後兩個 bits 變成 002struct fd
所以在將型態強制轉型成結構內的型態 struct foo *
,始能將 v
記憶體位址取址EXP1 = (struct foo *)(v & ~3)
測試:
結果:
可以看到在記憶體的表現上以 8 byte 的空間分配
考慮以下程式碼,能將輸入的 8 位元無號整數逐一位元反轉,如同 LeetCode 190. Reverse Bits 的描述。
0xCC
與 0xAA
在 8 位元下的二進位表示分別為 11001100
與 10101010
,利用 &
的運算子,可將對應位置的 bit 紀錄,也就是指定相對應的 bit 進行 shift0xCC
與 0xAA
的二進位取反:EXP2 = (x & 0x33) << 2
EXP2 = (x & 0x55) << 1
x = 0x35
為例,8-bit
二進位為 001101012,預期結果為 101011002x = (x >> 4) | (x << 4);
x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2);
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
延伸〈你所不知道的 C 語言:前置處理器應用篇〉,我們嘗試將 foreach 巨集 予以擴充,使得支援以下寫法:
預期輸出如下:
對應的巨集定義:
__VA_ARGS__
主要用於宣告任意的變數在 function 的引數中,方便在宣告時不使用太多不同型態的宣告(可變引數的宣告方式),在 C99 以後的版本新增的功能A macro can be declared to accept a variable number of arguments much as a function can. The syntax for defining the macro is similar to that of a function.
來源 : gcc 文件
foreach_int
函式中的每一個 int
的引數,觀察到函式定義中的 i
即代表每一個引數的變數,所以變數 i
為範例中的 (0, -1, 100, 0, -99)
unsigned _foreach_i = (((i) = ((int[]){__VA_ARGS__})[0]), 0);
((i) =((int[]){__VA_ARGS__})[0])
,前面的 int[]
表示整數的陣列宣告,放入的資料為 __VA_ARGS__
也就是說將函式的引數值都存入了陣列當中 (0, -1, 100, 0, -99)
,而 [0]
表示陣列第一個位置的元素,以範例來說是 0
_foreach_i
變數值為括弧後的 0
,就好像是宣告了某個變數的過程時,再宣告一個變數,_foreach_i
也不會拿到 i
的值_foreach_i < sizeof((int[]){__VA_ARGS__}) / sizeof(int)
右半部分的內容,計算整個陣列的記憶體長度,除上 int
的長度,也就是計算整個陣列元素的數量,所以就可以觀察到 _foreach_i
的變數用於走訪的次數,會根據傳入引數值的個數(i) = ((int[]){__VA_ARGS__, 0})[EXP4]
所以根據上述的分析,可以得知 [EXP4]
為整數陣列中元素的位置,需要走訪每一個元素,為持續遞增的變數,因此 EXP4
為 ++_foreach_i
,所以觀察 foreach_ptr
函式功能也相似,所以 EXP5
也為 ++_foreach_i
EXP4 = ++_foreach_i;
EXP5 = ++_foreach_i;
一開始想法比較簡單,會選擇 _foreach_i++
,但是經時跑過程式後發現會是錯誤的,原因是在下一次的迴圈時,變數 i
比 _foreach_i++
早賦值,因此再第一次迴圈的時候會重複 (i) = ((int[]){__VA_ARGS__, 0})[0]
的結果,將第一個引數重複走訪兩次
_foreach_i++
結果:typeof()
取得引數的變數型態並宣告一個陣列存取Another way to refer to the type of an expression is with typeof. The syntax of using of this keyword looks like sizeof, but the construct acts semantically like a type name defined with typedef.
const char *
處理方式透過 void *
可以彈性處理傳入的型態,可用於指向不同型態的變數,有暫存的效果,可以再還原p
(元素)為 NULL
且 i
的數量少於陣列元素的個數,所以為了確保函式能順利走訪每一個資料assert
情況產生,出現以下的訊息:針對 LeetCode 29. Divide Two Integers,以下是可能的實作:
在兩整數二進位的除法中也可以利用長除法的觀念下去執行,如圖
先看到 4 ~ 15 行中分別處理被除數與除數的正負號問題,在執行上將兩個未知的整數都先取正數,使用的方式是利用二補數的行為取正,先取反 ~
再 +1
再來看到 17 ~ 19 行中根據長除法的方式,除數要先從被除數最前端的位元開始進行,所以利用迴圈的方式檢查被除數與除數相差的位元位數
應該不用使用到迴圈進行位元數的判斷,可以使用
ctz
類的相關函式進行修改
另外,若除數為零的情況下,在 17 ~ 19 行的敘述中會進入無窮迴圈,為本題的缺陷之處
EXP6 = dvs << shift
EXP7
會與這個判斷式有關,對於被除數或是除數的增減有影響shift--
),確保每一輪的減法是大減小的情況res
儲存商的結果,觀察到 (unsigned int) 1 << shift
就是由第二層迴圈判斷的結果,將商數儲存,利用 |
運算子根據 shift
移位的結果將該輪可進行一次減法的 1
存下EXP7
中為每一輪除法的減法敘述,因此 EXP7
為EXP7 = dvd -= dvs << shift
unsigned int
儲存結果,因此要避免 overflow 的問題產生,所以把特例的情況將結果固定為 int
的最大數值結果例外的情況為當除數為 -1 或 1 的情況就會有這種狀況的發生,能讓
res
的最左邊第一個位置(對於有號整數來說是正負號的判斷位元)的位元為除數 -1 或 1
考慮 ilog32
函式可針對給定的 32 位元無號數,計算扣除開頭的 0
,最少需要多少位元才可儲存 (the minimum number of bits required to store an unsigned 32-bit value without any leading zero bits),以下是一個可能的實作:
1 << 4
= 100002 = 1610v >>= m
,進行下一階段 8 個位元的檢查EXP10
為 11
的 bitmask ,轉為 16 進制 = 0x3UEXP10 = (v > 0x3U) << 1
流程上需要 v >> m; 用於下一階段的檢查,但已是最後一次的判斷所以不用再移位
<< 0
沒有實質作用所以刪除,再將 ret 結果補上 1 或 0 剛好為判斷式的結果,因此EXP11 = ret += v > 1
考慮以下進行記憶體地址對齊 (alignment) 的巨集:
alignment
的動作,將 1 ~ 16 的數值,都改為 1610 也就是 100002最後的 4 個 bit 要為 0,若傳入 0 則要回傳 0~(NNN)
為相關作法NNN
產生 00002 的方法為對 11112(1510) 取反,如何找出 15 這個數字,就是由 16 - 1 得來如果要產生 000 的 bitmask 也可以利用 810 -> 10002,再 - 1 為 0111,最後取反得到 1000
NNN = MAX_ALIGNMENT - 1
&
計算子清零,理解後就可以觀察到前面的敘述: ((x) + MAX_ALIGNMENT - MMM)
, x + MAX_ALIGNMENT
將 x 加上不影響的最後 4 個位元的數值(保留第 5 位元 16 的值)再搭配清零的動作就可以得到 16 的數值MMM = 1
考慮以下巨集可得到二個表示式進行整數除法操作時,最接近的整數值:
注意: 當除數 (即 divisor
) 為負值時,行為沒有定義。
參考執行結果:
DIVIDE_ROUND_CLOSEST(7, 3) = 2
DIVIDE_ROUND_CLOSEST(6, 3) = 2
DIVIDE_ROUND_CLOSEST(5, 3) = 2
看到前三個敘述的判斷條件,分別為:
((typeof(x)) -1) > 0
((typeof(divisor)) -1) > 0
(((__x) > 0) == ((__d) > 0))
x
、 divisor
的型態轉型到 -1
上,分兩種情況為有號數與無號數,這個判斷式就是要確保在計算的時候皆為無號數,所以透過 -1
的強制轉型判斷結果為:
而第三項的判斷式,也是確保傳入的兩數皆為正數的情況下進行計算
/
在運行的方式為無條件捨去小數的形式,所以要透過對結果有四捨五入的結果產生,所以要對 /
結果 + - 除數的一半數值,也就是說要將被除數捨棄掉的數值進行處理
RRR = (__x) + ((__d) >> 1)
SSS = (__x) - ((__d) >> 1)
考慮以下計算整數開平方根的實作:
i_sqrt
函式的作用等同於 floor(sqrt(x))
首先看到 fls()
(find the last bit set)函式的作用,目的是要找到一數在二進位中由最低位至到最高位的 1
的位置,檢查的方式為產生 1 的 bitmask 在不同位置檢查,檢查的順序為 32 位元、 48 位元、 56 位元、 60 位元、 62 位元、 63 位元,遞增的順序為 +16 +8 +4 +2 +1,以 410 (1002) 為例
m
透過 fls()
,找出數字的最高位元位置,目的為了找出小於該數值最大二進位中完全平方數,(例如: 1, 4, 16, 64),用 & ~1UL
將 fls()
的最低位數去除首先一個數可假設
而在二進位中可將每一項的 表示為 或是 可由二進位的數值進行逼近,對 進行迭代,假設為:
m+1為迭代的下一位
決定 或是 ,假設
若 ,則表示 (表示 存在於平方數中),若無則 根據上式則為 ,而為防止迭代停止的狀況,則為更新目標數字的數值為,持續更新數值為:,其中
(由式 1 平方結果而來)
分別表示:
又因
可得在 m-1
時:
所以當 ,而 ,
根據上述的計算方式就可以發現對應題目中的變數
對應程式為:
XXX = y >>= 1
ZZZ = m >>= 2
所以當平方數在 x
存在則需要扣除,因此:
YYY = x -= b