# 2023q1 Homework2 (quiz2)
contributed by < `ShamrockLee` >
### 測驗 `1`
填答後,完整程式碼如下(省略 `#include <stdint.h>` ):
```c=
uint64_t next_pow2(uint64_t x)
{
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 1;
x |= x >> 8; // AAAA is 8
x |= x >> 16;
x |= x >> 32; // BBBB is 32
return x + 1; // CCCC is x + 1
}
```
這段程式碼的運作方式如下:
1. 以最高位元的值覆蓋更低位元的值。
2. 整個數加一,以得到「最高位的更高一位是 1 ,其他位都是 0 」的數。
以 `gcc -O2 -std=c99 -S next_pow2.c` 在 x86_64-linux 環境編譯得到的組合語言節錄如下:
```asm=
next_pow2:
.LFB0:
.cfi_startproc
movq %rdi, %rax
shrq %rax
orq %rdi, %rax
movq %rax, %rdx
shrq %rax
orq %rdx, %rax
movq %rax, %rdx
shrq %rdx
orq %rdx, %rax
movq %rax, %rdx
shrq %rdx
orq %rax, %rdx
movq %rdx, %rax
shrq %rax
orq %rax, %rdx
movq %rdx, %rax
shrq %rax
orq %rdx, %rax
movq %rax, %rdx
shrq %rdx
orq %rdx, %rax
movq %rax, %rdx
shrq $8, %rdx
orq %rax, %rdx
movq %rdx, %rax
shrq $16, %rax
orq %rax, %rdx
movq %rdx, %rax
shrq $32, %rax
orq %rdx, %rax
addq $1, %rax
ret
.cfi_endproc
```
而這段程式碼顯然能精簡為:
```c=
uint64_t next_pow2(uint64_t x)
{
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x |= x >> 32;
return x + 1;
}
```
對應的組合語言為:
```asm=
next_pow2:
.LFB0:
.cfi_startproc
movq %rdi, %rax
shrq %rax
orq %rax, %rdi
movq %rdi, %rax
shrq $2, %rax
orq %rdi, %rax
movq %rax, %rdi
shrq $4, %rdi
orq %rdi, %rax
movq %rax, %rdi
shrq $8, %rdi
orq %rax, %rdi
movq %rdi, %rax
shrq $16, %rax
orq %rax, %rdi
movq %rdi, %rax
shrq $32, %rax
orq %rdi, %rax
addq $1, %rax
ret
.cfi_endproc
```
以 GCC 和 Clang 有支援的 `__builtin_clzl` 改寫如下
```c=
uint64_t next_pow2(uint64_t x)
{
return 1ul << 64 - __builtin_clzl(x);
}
```
對應的組合語言為:
```asm=
next_pow2:
.LFB0:
.cfi_startproc
bsrq %rdi, %rdi
movl $64, %ecx
movl $1, %eax
xorq $63, %rdi
subl %edi, %ecx
salq %cl, %rax
ret
.cfi_endproc
```
能看到 `bsrq` (Bit Scan Reverse) 指令。
在 Linux 核心中, `include/linux/log2.h` 定義了 `ilog2` 用到了類似的實做技巧。
#### 延伸問題:無分支的 floor_log2_u32 與 ceil_log2_u32 實作(含例外處理)
一對一討論時,我未能實作出能維護的無分支 `floor_log2_u32` 與 `ceil_log2_u32` 函式。
以下是我在參考 `include/linux/ilog2.h` 的思路後,嘗試實作的過程與結果。
期望實作的兩個函式宣告如下:
```c=
#include <stdint.h>
int32_t floor_log2_u32(uint32_t x);
int32_t ceil_log2_u32(uint32_t x);
```
期望的行為如下:
\\[
\text{floor_log2_u32}(x) = \begin{cases}
\lfloor \log_2 x \rfloor & (x \gt 0) \\
-1 & (x = 0)
\end{cases}
(x \in \mathbb Z^+_0)
\\]
\\[
\text{ceil_log2_u32}(x) = \begin{cases}
\lceil \log_2 x \rceil & (x \gt 0) \\
-1 & (x = 0)
\end{cases}
(x \in \mathbb Z^+_0)
\\]
`include/linux/log2.h` 當中, `ilog2_u32` 實作如下:
```c=
static __always_inline __attribute__((const))
int __ilog2_u32(u32 n)
{
return fls(n) - 1;
}
```
`include/asm-generic/bitops/fls.h` 當中, `fls` 實作如下:
```c=
/**
* fls - find last (most-significant) bit set
* @x: the word to search
*
* This is defined the same way as ffs.
* Note fls(0) = 0, fls(1) = 1, fls(0x80000000) = 32.
*/
static __always_inline int fls(unsigned int x)
{
int r = 32;
if (!x)
return 0;
if (!(x & 0xffff0000u)) {
x <<= 16;
r -= 16;
}
if (!(x & 0xff000000u)) {
x <<= 8;
r -= 8;
}
if (!(x & 0xf0000000u)) {
x <<= 4;
r -= 4;
}
if (!(x & 0xc0000000u)) {
x <<= 2;
r -= 2;
}
if (!(x & 0x80000000u)) {
x <<= 1;
r -= 1;
}
return r;
}
```
將以上 `fls` 改為無分支、不使用 extension 的版本:
```c=
static inline uint32_t fls_u32(uint32_t x)
{
// x_pow2 is x with all non-msb zeroized
uint32_t x_pow2 = x >> 1;
x_pow2 |= x_pow2 >> 1;
x_pow2 |= x_pow2 >> 2;
x_pow2 |= x_pow2 >> 4;
x_pow2 |= x_pow2 >> 8;
// Consider that x might be 0.
x_pow2 += (x != 0);
return
// 0b01010101'01010101'01010101'01010101u
(((x_pow2 & 0x55555555u) != 0)) +
// 0b01100110'01100110'01100110'01100110u
(((x_pow2 & 0x66666666u) != 0) << 1) +
// 0b01111000'01111000'01111000'01111000u
(((x_pow2 & 0x78787878u) != 0) << 2) +
// 0b01111111'10000000'01111111'10000000u
(((x_pow2 & 0x7f807f80u) != 0) << 3) +
// 0b01111111'11111111'10000000'00000000u
(((x_pow2 & 0x7fff8000u) != 0) << 4) +
// 0b10000000'00000000'00000000'00000000u
(((x_pow2 & 0x80000000u) != 0) << 5);
}
```
此時
```c=
int32_t floor_log2_u32(uint32_t x)
{
return fls_u32(x) - 1;
}
```
以上實作很自然的滿足了 `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` 。
```c=
int32_t ceil_log2_u32(uint32_t x)
{
uint32_t mask_anybit = x;
mask_anybit |= mask_anybit >> 1;
mask_anybit |= mask_anybit << 1;
mask_anybit |= mask_anybit >> 2;
mask_anybit |= mask_anybit << 2;
mask_anybit |= mask_anybit >> 4;
mask_anybit |= mask_anybit << 4;
mask_anybit |= mask_anybit >> 8;
mask_anybit |= mask_anybit << 8;
mask_anybit |= mask_anybit >> 16;
mask_anybit |= mask_anybit << 16;
return (fls_u32(mask_anybit & (x - 1))) - (~mask_anybit & 1);
}
```
:::warning
TODO: 針對 `floor_log2_u32` 及 `ceil_log2_u32`,找出可共用的程式碼,並利用巨集來產生這二個函式。
:::
更新:
`ceil_log2_u32` 的例外處理能透過把兩處 `- 1` 分別換成 `(x != 0)` 與 `(x == 0)` ,不用花那麼多指令週期建立 `mask_anybit` 。
```c=
int32_t ceil_log2_u32(uint32_t x)
{
return fls_u32(x - (x != 0)) - (x == 0);
}
```
如此一來, `floor_log2_u32` 與 `ceil_log2_u32` 的共通處只剩下「都有呼叫 `fls_u32` 」。
對照〈[回顧 bitops 並改進](https://hackmd.io/@sysprog/ByS9w_lHh)〉,能拆解上述無分支的 `ffs` 函數以巨集定義,與 `fls` 共用下半部份程式碼,產生與 〈回顧 bitops 並改進〉 當中所提出的 `gen_ffs` 、 `gen_fls` 相容的巨集。
〈回顧 bitops 並改進〉引入使用 `_Pragma` 運算子包裝的 loop-unrolling directive ,使巨集能同時支援不同長度的輸入值,同時不必增加 branch 。但是當中提出的
```c
_Pragma("GCC unroll 6")
```
僅支援 GCC 編譯器。考慮到 Clang 也支援 loop unrolling ,能定義巨集
```c
#ifdef __clang__
#define unroll_ffls_branchless _Pragma("clang loop unroll_count(6)")
#elif __GNUC__ >= 8
#define unroll_ffls_branchless _Pragma("GCC unroll 6")
#else
#define unroll_ffls_branchless
#endif
```
並將 `unroll_ffls_branchless` 加在 while loop 之前。這樣無論以 GCC 與 Clang 編譯,迴圈都能正確展開。
我曾嘗試寫出能接收輸入值以指定迴圈展開次數的巨集(類似 `pragma_unroll(N)` ),但受限於
* 十進位整數常數( decimal integer constant )沒有特定的前綴,無法用以保留前方的空格
* 括號(`()`)不適用 token pasting (`##`)
而一直無法實作成功。
根本的解決之道,大概是從 GCC 下手,使其支援 Clang 的 [`#pragma unroll N`](https://clang.llvm.org/docs/AttributeReference.html#pragma-unroll-pragma-nounroll) ,而不必再以 conditional directives 繞過編譯器的差異。相關的 [feature request](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=91840) 在 GCC 的 bug tracker 當中有人提出,或許暑假能試著實作。
雖然如此,在嘗試過程中發現了 Linux 核心內網路與網路 driver 相關程式的註解中, `transmit` 與其變化形不時被拼錯成 `tranmit` ,因此送了一組 patches 到 Linux 核心,已有部份獲得維護者採納,進入 `net-next` source tree 。
觀察上方的 `fls_u32` 能發現函數實作由兩部份構成:
1. 將輸入值最高非零位元以外的位元都設成零。
2. 以二分搜尋定位出非零位元的索引。
將 `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`:
```c
/* Preserve the most significant bit set of X and unset others.
* Return zero when non set.
*/
#define __preserve_msb_branchless(X, type) \
do { \
type __is_nonzero = !!(X); \
(X) = (X) >> 1; \
unroll_ffls_branchless for (size_t _i_exp = 1; \
_i_exp < (sizeof(X) << 3); _i_exp <<= 1) \
{ \
(X) |= (X) >> _i_exp; \
} \
(X) += __is_nonzero; \
} while (0)
```
`__preserve_lsb_branchless`:
```c
/* Preserve the least significant bit set of X and unset others.
* Return zero when non set.
*/
#define __preserve_lsb_branchless(X, type) \
do { \
unroll_ffls_branchless for (size_t _i_exp = 1; \
_i_exp < (sizeof(X) << 3); _i_exp <<= 1) \
{ \
(X) |= (X) << _i_exp; \
} \
type _x_upperfilled = (X); \
(X) = ~(X) + (!!_x_upperfilled); \
(X) &= _x_upperfilled; \
} while (0)
```
`__locate_bit_branchless`:
```c
/* Take in an unsigned integer with at most one bit set, and then
* output the 1-based digit of that bit. Return 0 if no bits are set.
*
* Bits higher than (2**64 - 1) will be truncated.
*
* Determine the result bit value through binary search with the following
* masks:
*
* 0b01010101'01010101'01010101'01010101'01010101'01010101'01010101'01010101u
* 0b01100110'01100110'01100110'01100110'01100110'01100110'01100110'01100110u
* 0b01111000'01111000'01111000'01111000'01111000'01111000'01111000'01111000u
* 0b01111111'10000000'01111111'10000000'01111111'10000000'01111111'10000000u
* 0b01111111'11111111'10000000'00000000'01111111'11111111'10000000'00000000u
* 0b01111111'11111111'11111111'11111111'10000000'00000000'00000000'00000000u
* 0b10000000'00000000'00000000'00000000'00000000'00000000'00000000'00000000u
*/
#define __locate_bit_branchless(x_pow2, in_type, out_type) \
((out_type) (((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x5555555555555555u)))) \
<< 0) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x6666666666666666u)))) \
<< 1) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x7878787878787878u)))) \
<< 2) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x7f807f807f807f80u)))) \
<< 3) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x7fff80007fff8000u)))) \
<< 4) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x7fffffff80000000u)))) \
<< 5) + \
((!!((x_pow2) & ((in_type) (((in_type) ~(in_type) 0) & \
0x8000000000000000u)))) \
<< 6)))
```
`gen_fls_branchless`:
```c
/* Branchless version of gen_fls */
#define gen_fls_branchless(in_type, out_type, start_from, fn_name) \
static __always_inline out_type fn_name(in_type x) \
{ \
__preserve_msb_branchless(x, in_type); \
out_type _ret = __locate_bit_branchless(x, in_type, out_type); \
return _ret - (out_type) ((!(start_from)) & (!(_ret))); \
}
}
```
`gen_ffs_branchless`:
```c
/* Branchless version of gen_ffs */
#define gen_ffs_branchless(in_type, out_type, start_from, fn_name) \
static __always_inline out_type fn_name(in_type x) \
{ \
__preserve_lsb_branchless(x, in_type); \
out_type _ret = __locate_bit_branchless(x, in_type, out_type); \
return _ret - (out_type) ((!(start_from)) & (!(_ret))); \
}
```
由於界面與〈回顧 bitops 並改進〉的 `gen_fls` 、 `gen_ffs` 一致,故能套用其方法一併定義 `clz` 、 `ctz` 等相關函數。