linux2022
contributed by <ganoliz>
考慮以下對二個無號整數取平均值的程式碼:
這個直覺的解法會有 overflow 的問題,若我們已知 a, b 數值的大小,可用下方程式避免 overflow:
接著我們可改寫為以下等價的實作:
我們再次改寫為以下等價的實作:
請問,
EXP1 = ?
EXP2 = ?
EXP3 = ?
根據左移算子我們可以知道a>>1與b>>1相當於是除以二,那這裡會遇到一個問題是當a為奇數且b為奇數的情況會造成出來的平均會少1。當發生這個情況的時候我們直覺是使用if-else的branch來實作,但這樣就等於沒有用到 bit-wise 操作的快速與不用進行分支預測的好處了。
根據數位邏輯
a | b | result |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
於是我們只需要把 a,b 最後 1 bits & 起來即可,因此 EXP1 = a & b & 1
EXP2與EXP3 我首先使用 bit wise 的寫法,使用 a & b 來當作進位, a | b 當作加法。
得到此式 :
顯然相當不漂亮,EXP2 這樣寫是為了配合 EXP3 進行 or 運算所損失的數值才寫成這樣。但是的確很不符合 bit-wise 的作法,因為 and 也要位移再做加法才得到最後的進位。後來想起數位邏輯的加法器
加法使用 xor 運算來實現,進位則由 and 運算來負責,因此重寫 EXP2 及 EXP3
延伸問題:
1.解釋上述程式碼運作的原理
2.比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
3.研讀 Linux 核心原始程式碼 include/linux/average.h, 探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用。
改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max):
請問,
EXP4 = ?
EXP5 = ?
根據式子,
根據EXP5 ,我們假設為 a < b ,當 a > b 時,右式得到的值是0,因此輸出為 a 。若 a < b 則根據 uint32_t 輸出為 0xFFFF (有號位整數的 -1),因此右式的值會變為 EXP4 的值,也就代表 a ^ EXP4 ,簡單思考一下可以知道 b - a 、 a + b 的值都可以用 xor 表示(不考慮借位與進位),因此 EXP4 應為 a ^ b ,根據數位邏輯的布林代數驗證一下也可以知道 a ^ a ^ b 為 b 。
因此答案為:
延伸問題:
請在 Linux 核心原始程式碼中,找出更多類似的實作手法。請善用 git log 檢索。
考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式:
改寫為以下等價實作:
請問,
COND = ?
RET = ?
首先我們先看這個 condition :
因此,shift 這個 for 迴圈在做的事就是將 u 跟 v 共同的二倍數找出來。
當我們找完之後就是把還有二倍數的任一數除乾淨(因為我們已經不再會有二倍數的公因數了),
接著進入餘數定理:
因此,當我們的 v 小於 1時就應該結束,所以跟一開始的程式結束條件是一樣的 while (v)。
然後我們要 return 的值是 u 乘以左移shift的次數(二的次方): u * (1 << shift)。
延伸問題:
在影像處理中,bit array (也稱 bitset) 廣泛使用,考慮以下程式碼:
考慮 GNU extension 的 __builtin_ctzll 的行為是回傳由低位往高位遇上連續多少個 0 才碰到 1。
範例: 當 a = 16
16 這個十進位數值的二進位表示法為 00000000 00000000 00000000 00010000 低位元 (即右側) 往高位元,我們可發現
0→0→0→0→1 ,於是 ctz 就為 4,表示最低位元往高位元有 4 個 0
用以改寫的程式碼如下:
其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b ,那 t 就會變成 000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。
若 bitmap 越鬆散 (即 1 越少),於是 improved 的效益就更高。
請問,
EXP6 = ?
首先,我參考了 Introduction to Low Level Bit Hacks 提供的這篇文章,我們可以知道怎麼 get right-most bit value 。
即是使用:
因此,EXP6 :
其實我一開始是想到使用:
不過轉換後其實兩者布林代數是相等的,而且前者的可讀性更好。
延伸問題:
以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作:
請問,
PPP = ? (表示式)
MMM = ? (巨集)
EEE = ? (表示式)
首先我們可以看看 Hash Table 的結構,我們可以知道我們使用 remainder 當作 Key 來查表,當我們有符合的 Key 在該項 Hash Table 裡時則回傳該 node 的 index。因此我們可以先解 MMM 與 EEE 是:
透過 HashTable 映射到對應的 Hlist_head 來搜尋 key 值,因此我們儲存節點就要依此邏輯來存取。
現在我們考慮 PPP ,
當我們無法從 Hash Table 找到位置時,代表我們尚未有該值,建立一個 hash table 的 element 下次方便找 index 值,然後便將值寫入在 *decimal ( 也就是 q 目前的位置 ),並往下一位數前進。
因此當我們 find 到 pos 的時候,代表我們在這個位數的值等於前面某一個位數的值(已經確定有循環小數了),此時我們應該先將循環小數前的位數寫入 result ,因此 PPP 應為(原本我想的條件是 pos - i > 0
但一行解決的確是 pos– ):
最後再將循環位數利用下一個 while 迴圈填入左括號與右括號之間,直到 decimal 為 "\0" 。
延伸問題:
解釋上述程式碼運作原理,指出其中不足,並予以改進
在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
alignof 是 GNU extension,以下是其可能的實作方式:
請問,
M = ?
X = ?
首先,這段程式雖然短卻不好了解,先拆開來
我們宣告一個 struct 指標, 0 是虛擬地址(正如同offsetof"它假定結構的位址為0,然後獲得成員的偏移值")
重新複習運算子優先順序因此可以知道我們先取 成員 M 再取得它的位置,接著:
就是將我們取得的位置轉型成 char 指標再跟 X 這個char 指標相減。我們要使用 struct 裡面 佔最大的成員當成 alignment 的大小,那由於 char 指標只佔 1 bit ,我們只要看 _h 的大小來決定是幾位元。
因此,M 應為 _h。 而 X 應為 0 (為 struct 的開頭成員 char c 的位置) ,透過相減取得 _h 成員的偏移。
延伸問題:
考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目:
觀察 printf 的(格式)字串,可分類為以下三種:
我們若能精準從給定輸入 i 的規律去控制 start 及 length,即可符合 FizzBuzz 題意:
以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: (bitwise.c)
其中 is_divisible 函式技巧來自 Faster remainders when the divisor is a constant: beating compilers and libdivide,甚至 gcc-9 還內建了 FizzBuzz optimization (Bug 82853 - Optimize x % 3 == 0 without modulo)。
請補完,使得程式碼符合預期,儘量以最簡短且符合一致排版的形式來撰寫。
請問,
KK1 = ? (變數名稱)
KK2 = ? (變數名稱)
KK3 = ? (表示式,不包含小括號)
我們可以知道 FizzBuzz 的長度變化是跟是否能被 3 與 5 整除來增長的,因此我們可以從 div3 與 div5 得到是否整除的關係,因此 KK1 = div3 , KK2 = div5 兩者其實可以更換。
至於 KK3 則是針對 FizzBuzz 字串要從哪裡複製來做位移。因為字串從起始位置 9 開始,往前四格開始就是 divide 2 因此假如 div5 為 1 就從起始位置 4 開始。至於 KK3 的地方的話,因為只要 div3 為 1 的情況,就應該從起始位置 0 的地方開始,因此 KK3 應為 div3 * 4 直接讓起始位置歸零。
延伸問題: