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_iEXP4 = ++_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) = 2DIVIDE_ROUND_CLOSEST(6, 3) = 2DIVIDE_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