contributed by < ShamrockLee
>
1
填答後,完整程式碼如下(省略 #include <stdint.h>
):
這段程式碼的運作方式如下:
以 gcc -O2 -std=c99 -S next_pow2.c
在 x86_64-linux 環境編譯得到的組合語言節錄如下:
而這段程式碼顯然能精簡為:
對應的組合語言為:
以 GCC 和 Clang 有支援的 __builtin_clzl
改寫如下
對應的組合語言為:
能看到 bsrq
(Bit Scan Reverse) 指令。
在 Linux 核心中, include/linux/log2.h
定義了 ilog2
用到了類似的實做技巧。
一對一討論時,我未能實作出能維護的無分支 floor_log2_u32
與 ceil_log2_u32
函式。
以下是我在參考 include/linux/ilog2.h
的思路後,嘗試實作的過程與結果。
期望實作的兩個函式宣告如下:
期望的行為如下:
include/linux/log2.h
當中, ilog2_u32
實作如下:
include/asm-generic/bitops/fls.h
當中, fls
實作如下:
將以上 fls
改為無分支、不使用 extension 的版本:
此時
以上實作很自然的滿足了 floor_log2_u32(0) == -1
,因此無須另外處理。
對於正整數 x
, ceil_log2_u32(x)
能被定義為 fls_u32(x - 1)
。
但在 C 標準中, -1 在轉型為無號整數的行為未定義。因此,遵守標準的實作必須處理 x
為 0 時的例外情況。
以下實作使用 mask_anybit
作為處理例外的位元遮罩。如果 x 有任何不為零的位元, mask_anybit 就會是 0xffffffff
,否則是 0
。
TODO: 針對 floor_log2_u32
及 ceil_log2_u32
,找出可共用的程式碼,並利用巨集來產生這二個函式。
更新:
ceil_log2_u32
的例外處理能透過把兩處 - 1
分別換成 (x != 0)
與 (x == 0)
,不用花那麼多指令週期建立 mask_anybit
。
如此一來, floor_log2_u32
與 ceil_log2_u32
的共通處只剩下「都有呼叫 fls_u32
」。
對照〈回顧 bitops 並改進〉,能拆解上述無分支的 ffs
函數以巨集定義,與 fls
共用下半部份程式碼,產生與 〈回顧 bitops 並改進〉 當中所提出的 gen_ffs
、 gen_fls
相容的巨集。
〈回顧 bitops 並改進〉引入使用 _Pragma
運算子包裝的 loop-unrolling directive ,使巨集能同時支援不同長度的輸入值,同時不必增加 branch 。但是當中提出的
僅支援 GCC 編譯器。考慮到 Clang 也支援 loop unrolling ,能定義巨集
並將 unroll_ffls_branchless
加在 while loop 之前。這樣無論以 GCC 與 Clang 編譯,迴圈都能正確展開。
我曾嘗試寫出能接收輸入值以指定迴圈展開次數的巨集(類似 pragma_unroll(N)
),但受限於
()
)不適用 token pasting (##
)而一直無法實作成功。
根本的解決之道,大概是從 GCC 下手,使其支援 Clang 的 #pragma unroll N
,而不必再以 conditional directives 繞過編譯器的差異。相關的 feature request 在 GCC 的 bug tracker 當中有人提出,或許暑假能試著實作。
雖然如此,在嘗試過程中發現了 Linux 核心內網路與網路 driver 相關程式的註解中, transmit
與其變化形不時被拼錯成 tranmit
,因此送了一組 patches 到 Linux 核心,已有部份獲得維護者採納,進入 net-next
source tree 。
觀察上方的 fls_u32
能發現函數實作由兩部份構成:
將 fls
與 ffs
比較,能發現兩者差異僅在得知非零最高位元或非零最低位元的索引,所以只要改動「將其他位元設成零」的步驟,而能共用定位的步驟。因此,以下將以三個巨集實作這兩種函數:
__preserve_msb_branchless
負責「保留非零最高位元」的無分支實作__preserve_lsb_branchless
負責「保留非零最低位元」的無分支實作__locate_bit_branchless
負責「定位非零位元索引」的無分支實作〈回顧 bitops 並改進〉對 Linux 核心當中現有實作分析後,發現了「索引從 1 開始」與「索引從 0 零開始」兩種風格,並以「回傳 0 」處理「索引從 0 開始」風格的例外(輸入值為 0 的情形)。這樣一來,「從 0 開始」在輸入值為 0 、為 1 時會對應到同樣的值,而有資訊損失。因此我針對「從 1 的情形」實作運算步驟,並在 gen_fls_branchless
與 gen_ffs_branchless
內處理「從 0 開始」的例外。
以下是實作的程式碼:
__preserve_msb_branchless
:
__preserve_lsb_branchless
:
__locate_bit_branchless
:
gen_fls_branchless
:
gen_ffs_branchless
:
由於界面與〈回顧 bitops 並改進〉的 gen_fls
、 gen_ffs
一致,故能套用其方法一併定義 clz
、 ctz
等相關函數。