contributed by < jj97181818
>
(a >> 1) + (b >> 1) + (a & b & 1) | |
---|---|
a = 2, b = 2 | 1 + 1 + 0 |
a = 2, b = 3 | 1 + 1 + 0 |
a = 3, b = 2 | 1 + 1 + 0 |
a = 3, b = 3 | 1 + 1 + 1 |
在 a 和 b 右移 1 位之後,最低位元可能會出現需要進位的狀況,因此要將最低位元考慮進來,也就是加上第三項 EXP1
= a & b & 1
。
在 你所不知道的 C 語言:數值系統篇 有提到用加法器來思考:x + y = x ^ y + (x & y) << 1
其中,x ^ y
可求得位元相加不進位的總和,x & y
則求得進位,並將進位的值左移一位,與總和相加。
現在要求 (x + y) / 2
就是將 x ^ y + (x & y) << 1
整個右移一位,求得 ((x ^ y) >> 1) + (x & y)
,因此,EXP2 = a & b
, EXP3 = a ^ b
。
比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章)
研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用
先看 解讀計算機編碼 提供 min 的程式碼:
diff >> 31
為 0,(diff & (diff >> 31))
就會是 0,直接回穿 b。diff >> 31
32 個 bit 都是 1,因此 (diff & (diff >> 31))
就會是 diff
本身,所以 b + diff
= b + (a - b)
= a
,最後回傳 a。這題 max 的程式碼:
限制:
a ^ 0
會是 a 自己,而 a ^ (b - a)
會是 b(會寫 b - a
是參考 min 程式碼中 diff
的想法),所以 ((EXP4) & -(EXP5))
會是 0 或是 (b - a)
。((EXP4) & -(EXP5))
都會是 0,也就是最後會回傳 a,是 a 比較大的情況。a < b
,-(EXP5) 為 -1,每個 bit 皆為 1,任何值與 -1 做 AND 運算都還會是自己,也就是 ((EXP4) & -(EXP5))
的值會被 EXP4
決定。EXP4
會是 b - a
,但是限定使用 OR, AND, XOR。當 EXP4
為 a ^ b
時,回傳的值會是 a ^ (a ^ b)
,a 和 a 自己做 XOR 會是 0,0 再和 b 做 XOR 就會是 b 本身了。因此 EXP4
= a ^ b
, EXP5
= a < b
針對有號整數的實作和無號整數的實作一樣,因為這裡做的 XOR 的結果對於有號與無號沒有差別,後面 a < b
的部分就是按照兩者的大小比較,是 0 或 1,對有號無號也沒有影響。
請在 Linux 核心原始程式碼中,找出更多類似的實作手法,請善用 git log 檢索。
在 drivers/gpu/drm/radeon/r600_blit.c
的第 492 行有 branchless 的實作:
int2float 是將 int 轉成 float,這裡避免 branch 的方法是先用 __fls
找出 MSB 的位置,然後用旋轉指令將 unsigned int 擴展成浮點數。
也一併附上原本有 branch 的程式碼:
t = u - v
的差為零時,表示得到最大公因數,又 v = t
,因此COND
= v
。u
,但因為在做輾轉相除法之前有先將 u 除以 2(右移),因此要乘 2 shift 次(左移)。因此 COND
= v
, RET
= u << shift
__builtin_ctz
改寫 GCD,分析對效能的提升將原本判斷是偶數時,用迴圈不停地除以 2 的程式,都改成用 __builtin_ctz
算出從 LSB 到最低位元 1 之前的 0 的數量,然後透過右移來達到除以 2 的效果。
Linux 核心中也內建 GCD (而且還不只一種實作),例如 lib/math/gcd.c,請解釋其實作手法和探討在核心內的應用場景。
在 lib/math/gcd.c
中有實作兩種 GCD,如果能使用 __ffs
(find first bit in word),就用第一種 GCD 實作,反之用第二種。__ffs.h
會針對不同架構做不同的實作,通用版本的 __ffs.h
在 linux/include/asm-generic/bitops/__ffs.h
中:
在使用 __ffs.h
的 GCD 實作中,和以 __builtin_ctz
改寫 GCD 程式中的 __builtin_ctz
很像,只是換成使用 __ffs
(這裡的 __ffs
是從 0 開始從 LSB 算到最低位元 1,__builtin_ctz
是從 1 開始算到最低位元 1 的前一個,剛好結果會是一樣的)。
r & -r
和測驗 4 的答案 EXP6
= bitwise & -bitwise
是同樣意思,會留下最低位元的 1,其餘位元為 0。
這個程式是將 bitmap 陣列裡面所有 uint64_t 型別的整數中,bit 為 1 的位置都記錄到 out 裡面。
限制:
bitset ^= t;
應該要將 bitwise 該次找到最低位元的 1 改成 0,這裡的 t 要只有最低位元的 1,其餘位元都是 0,才能讓 bitset 和 t 做 XOR 之讓最低位元的 1 改為 0。t = bitwise & -bitwise
可以讓 t 只留下最低位元的 1。因此 EXP6
= bitwise & -bitwise
因為只儲存 1 的位置,所以可以用在壓縮儲存大小的場景,例如:壓縮圖片。
設計實驗,檢驗 ctz/clz 改寫的程式碼相較原本的實作有多少改進?應考慮到不同的 bitmap density;
閱讀 Data Structures in the Linux Kernel 並舉出 Linux 核心使用 bitmap 的案例
hash = key % size;
用 remainder % size 的方式決定要把節點塞在哪一個 heads 的 linked list。在這個 function 裡面,我們嘗試在特定 linked list 找尋是否有相同的餘數(key)的節點,如果有就回傳 index(value),如果找不到就回傳 -1。ramainder % size
來決定要在哪個 heads 的 linked list 找餘數。(remainder * 10) / d
是算出這位的小數數字,+ '0'
讓前面數字轉成字元,然後加到 dicimal
裡。(remainder * 10) % d
會算出下一輪的餘數。因此 PPP
= pos--
, MMM
= list_add
, EEE
= &heads[remainder % size]
第 58 行,不需要特別做 division > 0 的條件判斷:
因為在第 45 行已經處理過為負數的 n 和 d 了:
所以可以改成:
第 51 行的判斷正負號:
可以改成:
在 Linux 核心原始程式碼的 mm/ 目錄 (即記憶體管理) 中,找到特定整數比值 (即 fraction) 和 bitwise 操作的程式碼案例,予以探討和解說其應用場景
(struct { char c; t _h; } *)0
表示將 0 轉型成指向 struct { char c; t _h; }
的 pointer,他的特殊意義為空指標,但不能確定空指標指向的位置。(&((struct { char c; t _h; } *)0)->M)
是取得 struct 中某個成員 M
的位址(char *)(&((struct { char c; t _h; } *)0)->M)
是將 struct 中某成員 M
的位址轉型成 char*((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X)
就是將 struct 中某成員 M
的位址減去 X(char *)(&((struct { char c; t _h; } *)0)->M)
和 offsetof 一樣作用,後半減掉一個 X,X = 0。_h
需要比較大的 alignment 的話,那 char c
也需要 padding 到跟 _h
一樣的 alignment size,所以才會去算 char c
的大小(指定成員 _h
和 struct 起始位置的偏移量),所以這裡的 M 為 _h
,即可得到 t 型別 _h
alignment 的大小。因此 M
= _h
, X
=0
__alignof__
的使用案例 2 則針對其場景進行解說
在 linux/include/linux/kstrtox.h
的第 30 行:
kstrtoul 是將一個字串轉成 unsigned long,實作有用到 __alignof__
來判斷 unsigned long
和 unsigned long long
的 alignment 大小是否一樣。可能是要確保在該 data model 下 unsigned long
和 unsigned long long
的大小是一樣的,例如在 LP64 下,就都是 64 bit。
在 linux/crypto/aegis.h
的第 18 行:
有一個巨集 AEGIS_BLOCK_ALIGN
會取得 union aegis_block
的 alignment 大小。
ALIGN
, ALIGN_DOWN
, ALIGN_UP
等巨集探討其實作機制和用途,並舉例探討 (可和上述第二點的案例重複)
linux/tools/testing/selftests/vm/pkey-helpers.h
的第 180 行有 ALIGN_DOWN
和 ALIGN_UP
的巨集:linux/tools/testing/selftests/vm/hmm-tests.c
的第 54 行有 ALIGN
的巨集:ALIGN_UP 的參數 x
是原本的大小,align_to 是要用多少 bit 為倍數來對齊。
左段的程式 ((x) + ((align_to) - 1))
是將 x
加上 align_to
的大小再減去 1,讓原本大小不足 align_to
的部分,能向上進 1 位到 align_to
,例如:x
= 5, align_to
= 32,為了讓 5 能夠對齊變成 32 位元,就讓 5 + (32 - 1) = 36,讓值超過 32。
右段的程式 ~((align_to)-1))
是要去除不足 align_to 的餘數,因為最後算出來的值應該是 align_to 的倍數。延續上面的例子來解釋,就是 ~(32 - 1) 用二進位來表達 1110 0000,再與 36 做 AND 運算,會將不足 32 的部分都 clear 掉,就從 5 向上對齊成 32 了。
向下對齊不需要加上(align_to) - 1
來補足不到 align_to
的餘數,而是直接捨棄不到 align_to
的部分。同樣用 ~((align_to)-1))
去除不足 align_to
的餘數,最後算出來的值也是 align_to
的倍數。
在 linux 核心程式碼找到的 ALIGN 巨集看起來和向下對齊一樣。
unsigned int length = (2 << KK1) << KK2;
會先將 2(0010)左移 KK1 位,再左移 KK2 位。當 KK1 為 div3,KK2 為 div5 時,就可以根據 div3 和 div5 為 1(可被整除)或 0(不可被整除),來調整 length,如下表。左移位數 | length | |
---|---|---|
只被 3 整除 | 一位 | 0100 = 4 |
只被 5 整除 | 一位 | 0100 = 4 |
被 3 和 5 整除 | 兩位 | 1000 = 8 |
其餘 | 零位 | 0010 = 2 |
strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length);
的 9 要改成 8,因為一個數在不能被 3 和 5 整除的時候,是要將 "%u" 複製到 fmt 裡面,而 % 的位置在第 8 位。因此 KK1
= div3
, KK2
= div5
, KK3
= div3 << 2
避免 stream I/O 帶來的影響,可將 printf 更換為 sprintf
分析 Faster remainders when the divisor is a constant: beating compilers and libdivide 一文的想法 (可參照同一篇網誌下方的評論),並設計另一種 bitmask,如「可被 3 整除則末位設為 1」「可被 5 整除則倒數第二位設定為 1」,然後再改寫 bitwise.c 程式碼,試圖運用更少的指令來實作出 branchless;
- 參照 fastmod: A header file for fast 32-bit division remainders on 64-bit hardware
研讀 The Fastest FizzBuzz Implementation 及其 Hacker News 討論,提出 throughput (吞吐量) 更高的 Fizzbuzz 實作
解析 Linux 核心原始程式碼 kernel/time/timekeeping.c 裡頭涉及到除法運算的機制,探討其快速除法的實作 (注意: 你可能要對照研讀 kernel/time/ 目錄的標頭檔和程式碼)
- 過程中,你可能會發現可貢獻到 Linux 核心的空間,請充分討論